js-confuser-vm 0.0.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.gitmodules +4 -0
  2. package/CHANGELOG.md +125 -2
  3. package/README.md +128 -53
  4. package/bench.ts +146 -0
  5. package/disassemble.ts +12 -0
  6. package/dist/build-runtime.js +41 -15
  7. package/dist/compiler.js +328 -181
  8. package/dist/disassembler.js +317 -0
  9. package/dist/index.js +7 -2
  10. package/dist/runtime.js +255 -176
  11. package/dist/template.js +258 -0
  12. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  13. package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
  14. package/dist/transforms/bytecode/dispatcher.js +266 -0
  15. package/dist/transforms/bytecode/macroOpcodes.js +3 -3
  16. package/dist/transforms/bytecode/resolveConstants.js +100 -0
  17. package/dist/transforms/bytecode/resolveLabels.js +21 -18
  18. package/dist/transforms/bytecode/resolveRegisters.js +216 -0
  19. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  20. package/dist/transforms/bytecode/specializedOpcodes.js +22 -12
  21. package/dist/transforms/bytecode/stringConcealing.js +110 -0
  22. package/dist/transforms/runtime/classObfuscation.js +43 -0
  23. package/dist/transforms/runtime/handlerTable.js +91 -0
  24. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  25. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  26. package/dist/types.js +42 -1
  27. package/dist/utils/ast-utils.js +14 -0
  28. package/dist/utils/op-utils.js +1 -2
  29. package/dist/utils/pass-utils.js +100 -0
  30. package/dist/utils/profile-utils.js +3 -0
  31. package/index.ts +22 -16
  32. package/jest.config.js +19 -2
  33. package/output.disassembled.js +41 -0
  34. package/package.json +2 -1
  35. package/src/build-runtime.ts +113 -78
  36. package/src/compiler.ts +2703 -2482
  37. package/src/disassembler.ts +329 -0
  38. package/src/index.ts +12 -2
  39. package/src/options.ts +8 -1
  40. package/src/runtime.ts +294 -180
  41. package/src/template.ts +265 -0
  42. package/src/transforms/bytecode/aliasedOpcodes.ts +5 -2
  43. package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
  44. package/src/transforms/bytecode/dispatcher.ts +292 -0
  45. package/src/transforms/bytecode/macroOpcodes.ts +4 -4
  46. package/src/transforms/bytecode/resolveLabels.ts +31 -27
  47. package/src/transforms/bytecode/resolveRegisters.ts +226 -0
  48. package/src/transforms/bytecode/specializedOpcodes.ts +27 -20
  49. package/src/transforms/bytecode/stringConcealing.ts +130 -0
  50. package/src/transforms/runtime/classObfuscation.ts +59 -0
  51. package/src/transforms/runtime/specializedOpcodes.ts +14 -9
  52. package/src/types.ts +106 -5
  53. package/src/utils/ast-utils.ts +19 -0
  54. package/src/utils/op-utils.ts +2 -2
  55. package/src/utils/pass-utils.ts +126 -0
  56. package/src/utils/profile-utils.ts +3 -0
  57. package/tsconfig.json +1 -1
  58. package/dist/transforms/utils/op-utils.js +0 -25
  59. package/dist/transforms/utils/random-utils.js +0 -27
  60. package/dist/utilts.js +0 -3
  61. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  62. package/src/transforms/runtime/internalVariables.ts +0 -270
  63. package/src/transforms/runtime/microOpcodes.ts +0 -93
  64. /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
@@ -0,0 +1,258 @@
1
+ // Template
2
+ // Compiles a JS code snippet into raw IR bytecode that can be spliced into the
3
+ // parent compiler's bytecode stream at any point before resolveRegisters /
4
+ // resolveLabels run.
5
+ //
6
+ // ── Usage ─────────────────────────────────────────────────────────────────────
7
+ //
8
+ // const tmpl = new Template(`
9
+ // function {name}(x, y) {
10
+ // return x + y;
11
+ // }
12
+ // `);
13
+ //
14
+ // const bc = tmpl.compile({ name: "myHelper" }, parentCompiler);
15
+ // result.push(...bc);
16
+ //
17
+ // ── How it works ──────────────────────────────────────────────────────────────
18
+ //
19
+ // 1. {name} placeholders are replaced with the caller-supplied string values.
20
+ // 2. A fresh child Compiler is created, inheriting the parent's OP table so
21
+ // opcode numbers match exactly (including randomizeOpcodes mappings).
22
+ // 3. The child compiles the snippet to raw IR (no passes, no label/register
23
+ // resolution).
24
+ // 4. Post-processing makes the child's bytecode compatible with the parent:
25
+ //
26
+ // Labels — every label string is renamed via parentCompiler._makeLabel()
27
+ // so names never collide with existing or future labels.
28
+ //
29
+ // FnIds — the child's main scope (fnDescriptors[0]) is mapped to
30
+ // targetFnId (default 0). Any inner functions (closures
31
+ // declared inside the template) are appended to
32
+ // parentCompiler.fnDescriptors with fresh indices.
33
+ //
34
+ // 5. The main function's entry defineLabel is stripped from the output — it is
35
+ // a synthetic wrapper added by _compileMain and is not part of the injected
36
+ // code. All other instructions (including the implicit RETURN at the end of
37
+ // the main scope and any inner-function blocks) are returned as-is so the
38
+ // caller can append them wherever appropriate.
39
+ //
40
+ // ── Limitations (MVP) ─────────────────────────────────────────────────────────
41
+ // • Variables are plain string/number interpolation only — no AST-node
42
+ // substitution.
43
+ // • Templates that reference upvalue-captured registers from the call site are
44
+ // not supported (inner functions closing over template-local variables work).
45
+ // • Opcodes with no JS equivalent (JUMP_REG, BXOR used as decode, etc.) cannot
46
+ // be expressed in a template; write those instruction arrays manually.
47
+
48
+ import { Compiler } from "./compiler.js";
49
+ import { DEFAULT_OPTIONS } from "./options.js";
50
+ export class Template {
51
+ constructor(source) {
52
+ this._source = source;
53
+ }
54
+
55
+ // ── String interpolation ──────────────────────────────────────────────────
56
+ _interpolate(variables) {
57
+ return this._source.replace(/\{(\w+)\}/g, (match, name) => {
58
+ if (!(name in variables)) {
59
+ throw new Error(`Template: missing variable {${name}}`);
60
+ }
61
+ return String(variables[name]);
62
+ });
63
+ }
64
+
65
+ // ── Main entry point ───────────────────────────────────────────────────────
66
+ /**
67
+ * Compile the template and return the inner (non-main) function descriptors
68
+ * and their bytecode blocks, ready to splice into the parent compiler's
69
+ * instruction stream.
70
+ *
71
+ * The template source should declare one or more named functions. The
72
+ * top-level ("main") scope of the template is discarded — it exists only as
73
+ * a syntactic wrapper so that function declarations parse correctly.
74
+ *
75
+ * Each inner function is registered in parentCompiler.fnDescriptors with a
76
+ * fresh fnIdx, and its bytecode block (defineLabel + body instructions) is
77
+ * returned so the caller can append it to the parent bytecode stream at the
78
+ * desired location (typically at the end, after all function bodies).
79
+ *
80
+ * @param variables Substitution map for {name} placeholders.
81
+ * @param parentCompiler The Compiler whose OP table, label counter, and
82
+ * fnDescriptors are shared.
83
+ *
84
+ * @returns
85
+ * functions — ordered list of inner FnDescriptors (index 0 = first named
86
+ * function in the template source). Use .entryLabel and
87
+ * ._fnIdx to build MAKE_CLOSURE operands.
88
+ * bytecode — IR bytecode blocks for all inner functions, ready to splice
89
+ * after the parent's function bodies. Does NOT include the
90
+ * template's main-scope instructions.
91
+ */
92
+ compile(variables, parentCompiler) {
93
+ // ── 1. Interpolate ────────────────────────────────────────────────────
94
+ const code = this._interpolate(variables);
95
+
96
+ // ── 2. Create child compiler, inherit parent's OP table ───────────────
97
+ // randomizeOpcodes is disabled — we copy the parent's already-randomized
98
+ // mapping directly so all opcode numbers are identical.
99
+ const child = new Compiler({
100
+ ...DEFAULT_OPTIONS,
101
+ randomizeOpcodes: false
102
+ });
103
+ child.OP = {
104
+ ...parentCompiler.OP
105
+ };
106
+ child.OP_NAME = {
107
+ ...parentCompiler.OP_NAME
108
+ };
109
+ child.JUMP_OPS = new Set(parentCompiler.JUMP_OPS);
110
+ child._makeLabel = parentCompiler._makeLabel.bind(parentCompiler);
111
+
112
+ // Record how many descriptors the parent already has so we can find the
113
+ // child's main (index = startIdx) and inner functions (startIdx+1 …).
114
+ const startIdx = parentCompiler.fnDescriptors.length;
115
+ child.fnDescriptors = parentCompiler.fnDescriptors; // share — inner functions auto-register
116
+
117
+ // ── 3. Compile to raw IR (no passes) ──────────────────────────────────
118
+ child.compile(code);
119
+
120
+ // parentCompiler.fnDescriptors[startIdx] → child's main (discard)
121
+ // parentCompiler.fnDescriptors[startIdx+1…] → inner helper functions
122
+ const innerDescs = parentCompiler.fnDescriptors.slice(startIdx + 1);
123
+
124
+ // Build bytecode blocks for inner functions only.
125
+ // child.bytecode was assembled by _compileMain from ALL fnDescriptors
126
+ // starting at startIdx. We rebuild it here from the inner descs only.
127
+ const innerBytecode = [];
128
+ for (const desc of innerDescs) {
129
+ innerBytecode.push([null, {
130
+ type: "defineLabel",
131
+ label: desc.entryLabel
132
+ }]);
133
+ for (const instr of desc.bytecode) {
134
+ innerBytecode.push(instr);
135
+ }
136
+ }
137
+ return {
138
+ functions: innerDescs,
139
+ bytecode: innerBytecode
140
+ };
141
+ }
142
+
143
+ // ── Inline compilation ───────────────────────────────────────────────────
144
+ /**
145
+ * Compile the template and return the **main scope** bytecode, with all
146
+ * register operands remapped to belong to `targetFnId`. This allows
147
+ * bytecode transforms to express high-level JS control flow (while-loops,
148
+ * if-chains, variable declarations) via Template and splice the result
149
+ * directly into an existing function's instruction stream.
150
+ *
151
+ * The implicit trailing RETURN added by _compileFunctionDecl is stripped —
152
+ * inline code should flow into the surrounding bytecode, not return.
153
+ *
154
+ * @param variables Substitution map for {name} placeholders.
155
+ * @param parentCompiler The Compiler whose OP table, label counter, and
156
+ * fnDescriptors are shared.
157
+ * @param targetFnId The function whose register file the template's
158
+ * registers should be remapped into.
159
+ * @param maxId Live map of max register id per fnId — updated
160
+ * in-place as new registers are allocated.
161
+ *
162
+ * @returns
163
+ * bytecode — main-scope IR (no entry defineLabel, no trailing RETURN),
164
+ * ready to splice into the target function's instruction stream.
165
+ * registers — mapping of JS variable names → remapped RegisterOperands,
166
+ * so the caller can reference template-declared variables
167
+ * (e.g. the `state` variable in CFF).
168
+ * functions — inner function descriptors (same as compile()).
169
+ * innerBytecode — inner function bytecode blocks (same as compile()).
170
+ */
171
+ compileInline(variables, parentCompiler, targetFnId, maxId) {
172
+ const code = this._interpolate(variables);
173
+ const child = new Compiler({
174
+ ...DEFAULT_OPTIONS,
175
+ randomizeOpcodes: false
176
+ });
177
+ child.OP = {
178
+ ...parentCompiler.OP
179
+ };
180
+ child.OP_NAME = {
181
+ ...parentCompiler.OP_NAME
182
+ };
183
+ child.JUMP_OPS = new Set(parentCompiler.JUMP_OPS);
184
+ child._makeLabel = parentCompiler._makeLabel.bind(parentCompiler);
185
+ const startIdx = parentCompiler.fnDescriptors.length;
186
+ child.fnDescriptors = parentCompiler.fnDescriptors;
187
+ child.compile(code);
188
+ const mainDesc = parentCompiler.fnDescriptors[startIdx];
189
+ const mainFnId = mainDesc._fnIdx;
190
+ const mainBc = mainDesc.bytecode;
191
+
192
+ // ── Remap registers from the template's main fnId → targetFnId ────────
193
+ // Build a mapping: old register id → new RegisterOperand in targetFnId.
194
+ const regRemap = new Map();
195
+ const remapReg = id => {
196
+ if (!regRemap.has(id)) {
197
+ const next = (maxId.get(targetFnId) ?? -1) + 1;
198
+ maxId.set(targetFnId, next);
199
+ regRemap.set(id, {
200
+ type: "register",
201
+ id: next,
202
+ fnId: targetFnId
203
+ });
204
+ }
205
+ return regRemap.get(id);
206
+ };
207
+ for (const instr of mainBc) {
208
+ for (let j = 1; j < instr.length; j++) {
209
+ const op = instr[j];
210
+ if (op && typeof op === "object" && op.type === "register" && op.fnId === mainFnId) {
211
+ const mapped = remapReg(op.id);
212
+ op.id = mapped.id;
213
+ op.fnId = mapped.fnId;
214
+ }
215
+ }
216
+ }
217
+
218
+ // ── Build variable name → remapped register mapping ───────────────────
219
+ const registers = new Map();
220
+ const locals = mainDesc.ctx.scope._locals;
221
+ for (const [name, reg] of locals) {
222
+ const mapped = regRemap.get(reg.id);
223
+ if (mapped) registers.set(name, mapped);
224
+ }
225
+
226
+ // ── Strip entry defineLabel and trailing implicit RETURN ───────────────
227
+ let bytecode = mainBc.filter(instr => {
228
+ const op0 = instr[1];
229
+ return !(instr[0] === null && op0?.type === "defineLabel" && op0.label === mainDesc.entryLabel);
230
+ });
231
+
232
+ // Remove trailing LOAD_CONST undefined + RETURN (implicit return added
233
+ // by _compileFunctionDecl).
234
+ const OP = parentCompiler.OP;
235
+ if (bytecode.length >= 2 && bytecode[bytecode.length - 1][0] === OP.RETURN && bytecode[bytecode.length - 2][0] === OP.LOAD_CONST) {
236
+ bytecode = bytecode.slice(0, -2);
237
+ }
238
+
239
+ // ── Inner function bytecode (same as compile()) ───────────────────────
240
+ const innerDescs = parentCompiler.fnDescriptors.slice(startIdx + 1);
241
+ const innerBytecode = [];
242
+ for (const desc of innerDescs) {
243
+ innerBytecode.push([null, {
244
+ type: "defineLabel",
245
+ label: desc.entryLabel
246
+ }]);
247
+ for (const instr of desc.bytecode) {
248
+ innerBytecode.push(instr);
249
+ }
250
+ }
251
+ return {
252
+ bytecode,
253
+ registers,
254
+ functions: innerDescs,
255
+ innerBytecode
256
+ };
257
+ }
258
+ }
@@ -1,4 +1,4 @@
1
- import { SOURCE_NODE_SYM } from "../../compiler.js";
1
+ import { OP_ORIGINAL, SOURCE_NODE_SYM } from "../../compiler.js";
2
2
  import { nextFreeSlot } from "../../utils/op-utils.js";
3
3
  import { shuffle } from "../../utils/random-utils.js";
4
4
 
@@ -41,6 +41,9 @@ export function aliasedOpcodes(bc, compiler) {
41
41
  const arity = instr.length - 1;
42
42
  if (arity < 1) continue; // 0-operand opcodes have nothing to permute
43
43
 
44
+ const opName = compiler.OP_NAME[op];
45
+ if (!OP_ORIGINAL[opName]) continue; // only consider original ops, not already-specialized ones
46
+
44
47
  const existing = opStats.get(op);
45
48
  if (!existing) {
46
49
  opStats.set(op, {