js-confuser-vm 0.1.0 → 0.1.2

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 (63) hide show
  1. package/README.md +281 -147
  2. package/dist/build-runtime.js +41 -15
  3. package/dist/compiler.js +714 -265
  4. package/dist/disassembler.js +367 -0
  5. package/dist/index.js +7 -2
  6. package/dist/runtime.js +160 -119
  7. package/dist/template.js +163 -42
  8. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  9. package/dist/transforms/bytecode/concealConstants.js +2 -2
  10. package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
  11. package/dist/transforms/bytecode/dispatcher.js +15 -111
  12. package/dist/transforms/bytecode/macroOpcodes.js +2 -2
  13. package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
  14. package/dist/transforms/bytecode/resolveRegisters.js +23 -4
  15. package/dist/transforms/bytecode/selfModifying.js +88 -21
  16. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  17. package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
  18. package/dist/transforms/bytecode/stringConcealing.js +288 -0
  19. package/dist/transforms/runtime/classObfuscation.js +43 -0
  20. package/dist/transforms/runtime/handlerTable.js +91 -0
  21. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  22. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  23. package/dist/types.js +1 -1
  24. package/dist/utils/ast-utils.js +75 -0
  25. package/dist/utils/op-utils.js +1 -2
  26. package/dist/utils/pass-utils.js +100 -0
  27. package/dist/utils/profile-utils.js +3 -0
  28. package/package.json +8 -1
  29. package/.gitmodules +0 -4
  30. package/.prettierignore +0 -1
  31. package/CHANGELOG.md +0 -335
  32. package/babel-plugin-inline-runtime.cjs +0 -34
  33. package/babel.config.json +0 -23
  34. package/index.ts +0 -38
  35. package/jest-strip-types.js +0 -10
  36. package/jest.config.js +0 -52
  37. package/src/build-runtime.ts +0 -78
  38. package/src/compiler.ts +0 -2593
  39. package/src/index.ts +0 -14
  40. package/src/minify.ts +0 -21
  41. package/src/options.ts +0 -18
  42. package/src/runtime.ts +0 -923
  43. package/src/template.ts +0 -141
  44. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
  45. package/src/transforms/bytecode/concealConstants.ts +0 -52
  46. package/src/transforms/bytecode/dispatcher.ts +0 -398
  47. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  48. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  49. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  50. package/src/transforms/bytecode/resolveRegisters.ts +0 -221
  51. package/src/transforms/bytecode/selfModifying.ts +0 -121
  52. package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
  53. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  54. package/src/transforms/runtime/internalVariables.ts +0 -270
  55. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  56. package/src/transforms/runtime/microOpcodes.ts +0 -93
  57. package/src/transforms/runtime/minify.ts +0 -1
  58. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  59. package/src/transforms/runtime/specializedOpcodes.ts +0 -156
  60. package/src/types.ts +0 -93
  61. package/src/utils/op-utils.ts +0 -48
  62. package/src/utils/random-utils.ts +0 -31
  63. package/tsconfig.json +0 -12
@@ -1,121 +0,0 @@
1
- import type { Bytecode, Instruction } from "../../types.ts";
2
- import { Compiler } from "../../compiler.ts";
3
- import { choice } from "../../utils/random-utils.ts";
4
- import { getInstructionSize } from "../../utils/op-utils.ts";
5
-
6
- export function selfModifying(
7
- bc: Bytecode,
8
- compiler: Compiler,
9
- ): { bytecode: Bytecode } {
10
- // Walk the bytecode looking for "defineLabel" pseudo-ops, which start basic
11
- // blocks. For each block we collect the body (instructions between the label
12
- // and the next label/jump terminator), move it to the end of the bytecode
13
- // under a fresh "patch_LXX" label, and replace it in-place with:
14
- //
15
- // defineLabel ("originalLabel") ← kept as-is (pseudo-op)
16
- // PATCH destPc sliceStart sliceEnd ← 4 flat slots total
17
- // Garbage Opcodes × bodyFlatSize ← placeholder slots
18
- //
19
- // PATCH reads three inline operands via _operand():
20
- // destPc = originalLabel + 4 (first slot after PATCH's own 4 slots)
21
- // sliceStart = patchLabel (flat PC of appended body)
22
- // sliceEnd = patchLabel + bodyFlatSize
23
- //
24
- // On first execution PATCH copies bytecode[sliceStart..sliceEnd) over the
25
- // placeholder region starting at destPc. Execution then falls through into
26
- // the freshly-patched body. Subsequent calls are idempotent.
27
-
28
- const { OP, JUMP_OPS } = compiler;
29
-
30
- const result: Bytecode = [];
31
- const appended: Bytecode = [];
32
- let patchCount = 0;
33
-
34
- let i = 0;
35
- while (i < bc.length) {
36
- const instr = bc[i];
37
- const [op, operand] = instr;
38
-
39
- // Detect a defineLabel pseudo-op — start of a new basic block.
40
- if (
41
- op === null &&
42
- operand !== null &&
43
- typeof operand === "object" &&
44
- (operand as any).type === "defineLabel"
45
- ) {
46
- const originalLabel = (operand as any).label as string;
47
- result.push(instr); // keep the defineLabel marker
48
- i++;
49
-
50
- // Collect body: everything after the label until the next terminator.
51
- let j = i;
52
- while (j < bc.length) {
53
- const [nextOp, nextOperand] = bc[j];
54
-
55
- // Another defineLabel = boundary of the next block.
56
- if (
57
- nextOp === null &&
58
- typeof nextOperand === "object" &&
59
- (nextOperand as any)?.type === "defineLabel"
60
- ) {
61
- break;
62
- }
63
-
64
- // Jump instructions, RETURN all terminate the body.
65
- if (nextOp !== null && (JUMP_OPS.has(nextOp) || nextOp === OP.RETURN)) {
66
- break;
67
- }
68
-
69
- j++;
70
- }
71
-
72
- const body = bc.slice(i, j);
73
- const N = body.length;
74
-
75
- if (N === 0) {
76
- // Nothing to transform — label is immediately followed by a terminator.
77
- continue;
78
- }
79
-
80
- const patchLabel = `patch_${originalLabel}_${patchCount++}`;
81
-
82
- // Flat size of the body (each instruction occupies instr.length slots).
83
- const bodyFlatSize = body.reduce(
84
- (acc, instr) => acc + getInstructionSize(instr),
85
- 0,
86
- );
87
-
88
- // ── PATCH instruction (4 flat slots: opcode + 3 operands) ───────────
89
- // destPc = originalLabel + 4 (slot right after PATCH's 4 slots)
90
- // sliceStart = patchLabel
91
- // sliceEnd = patchLabel + bodyFlatSize
92
- result.push([
93
- OP.PATCH as number,
94
- { type: "label", label: originalLabel, offset: 4 },
95
- { type: "label", label: patchLabel },
96
- { type: "label", label: patchLabel, offset: bodyFlatSize },
97
- ] as unknown as Instruction);
98
-
99
- // ── Placeholders (Garbage Opcodes * bodyFlatSize, each 1 flat slot) ────────────
100
- // These are overwritten by PATCH on first execution.
101
- for (let p = 0; p < bodyFlatSize; p++) {
102
- const randomOpcode = choice(Object.values(compiler.OP));
103
- result.push([+randomOpcode]);
104
- }
105
-
106
- // ── Append real body at end ─────────────────────────────────────────
107
- appended.push([null, { type: "defineLabel", label: patchLabel }]);
108
- for (const bodyInstr of body) {
109
- appended.push(bodyInstr);
110
- }
111
-
112
- i = j; // skip over the original body in the input array
113
- continue;
114
- }
115
-
116
- result.push(instr);
117
- i++;
118
- }
119
-
120
- return { bytecode: [...result, ...appended] };
121
- }
@@ -1,153 +0,0 @@
1
- import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
2
- import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
3
- import { getInstructionSize, nextFreeSlot } from "../../utils/op-utils.ts";
4
-
5
- export const nSizedOps = [
6
- "MAKE_CLOSURE",
7
- "BUILD_ARRAY",
8
- "BUILD_OBJECT",
9
- "CALL",
10
- "CALL_METHOD",
11
- "NEW",
12
- ];
13
-
14
- // Creates specialized opcodes for the most frequent (OPCODE + single_integer_operand) pairs.
15
- // Example: [OP.LOAD_CONST, 1] becomes [SPECIALIZED_LOAD_CONST_1].
16
- // Only instructions that are fixed-sized are considered.
17
- // MAKE_CLOSURE and other N-sized instructions cannot be specialized
18
- // Operands are converted into objects and marked as 'placeholder' - other passes can mutate and the reference stays intact
19
- // We need a reference throughout the pipeline so that final AST generation can place the actual value
20
- // The 'placeholder' flag drops the operand from the final bytecode - any size calculation must not count these
21
- export function specializedOpcodes(
22
- bc: Bytecode,
23
- compiler: Compiler,
24
- ): { bytecode: Bytecode } {
25
- const disallowedOps = new Set(nSizedOps.map((name) => compiler.OP[name]));
26
-
27
- // ── Step 1: count frequency of eligible (op, operand) pairs ───────────────
28
- const freqMap = new Map<
29
- string,
30
- {
31
- op: number;
32
- operands: InstrOperand[];
33
- operandsKey: string;
34
- occurences: number;
35
- }
36
- >();
37
-
38
- for (const instr of bc) {
39
- const op = instr[0];
40
- if (op === null || disallowedOps.has(op)) continue;
41
-
42
- // Only supports between 1-6 operands
43
- const operandCount = getInstructionSize(instr) - 1;
44
- if (operandCount < 1 || operandCount > 6) continue;
45
-
46
- // Convert numbers into operand objects so they can be modified elsewhere and preserved
47
- const oldOperands = instr.slice(1);
48
- const operands = oldOperands.map((operand) => {
49
- if (typeof operand === "number") {
50
- return {
51
- type: "number",
52
- value: operand,
53
- resolvedValue: operand,
54
- } as InstrOperand;
55
- }
56
- return operand;
57
- });
58
-
59
- instr.length = 1;
60
- instr.push(...operands);
61
-
62
- const operandsKey = JSON.stringify(operands);
63
-
64
- const key = `${op},${operandsKey}`;
65
- const entry = freqMap.get(key);
66
- if (entry) {
67
- entry.occurences++;
68
- } else {
69
- freqMap.set(key, {
70
- op,
71
- operands,
72
- operandsKey,
73
- occurences: 1,
74
- });
75
- }
76
- }
77
-
78
- // ── Step 2: keep combinations that appear >= 2 times, sort by frequency ───
79
- const candidates = Array.from(freqMap.values())
80
- .filter((e) => e.occurences >= 1)
81
- .sort((a, b) => b.occurences - a.occurences);
82
-
83
- if (candidates.length === 0) return { bytecode: bc };
84
-
85
- // ── Step 3: assign free opcode slots to the best candidates ───────────────
86
- const sigToSpecial = new Map<string, number>();
87
- const specializedOps: Compiler["SPECIALIZED_OPS"] = {};
88
-
89
- for (let i = 0; i < candidates.length; i++) {
90
- const specialOp = nextFreeSlot(compiler);
91
- if (specialOp === -1) break;
92
- const { op: originalOp, operands, operandsKey } = candidates[i];
93
-
94
- const key = `${originalOp},${operandsKey}`;
95
- sigToSpecial.set(key, specialOp);
96
-
97
- specializedOps[specialOp] = { originalOp, operands };
98
-
99
- // Register a human-readable name for disassembly / debugging
100
- const originalName = compiler.OP_NAME[originalOp] ?? `OP_${originalOp}`;
101
- compiler.OP_NAME[specialOp] =
102
- `${originalName}_${JSON.stringify(operandsKey)}`;
103
- }
104
-
105
- // Store mapping so the interpreter knows how to dispatch the specialized op
106
- compiler.SPECIALIZED_OPS = specializedOps;
107
-
108
- // ── Step 4: replace matching instructions with the new single-byte opcode ─
109
- const result: Bytecode = [];
110
-
111
- for (const instr of bc) {
112
- const op = instr[0];
113
- // Only consider instructions with one or more operands
114
- if (op === null || instr.length <= 1 || op === compiler.OP.MAKE_CLOSURE) {
115
- result.push(instr);
116
- continue;
117
- }
118
-
119
- const operands = instr.slice(1);
120
- const operandsKey = JSON.stringify(operands);
121
-
122
- const key = `${op},${operandsKey}`;
123
-
124
- const specialOpCode = sigToSpecial.get(key)!;
125
-
126
- if (!specialOpCode) {
127
- result.push(instr);
128
- continue;
129
- }
130
-
131
- const newOperands = operands.map((operand) => {
132
- const operandAsObject: any =
133
- typeof operand === "object" && operand
134
- ? operand
135
- : {
136
- type: "number",
137
- resolvedValue: operand,
138
- };
139
-
140
- operandAsObject.placeholder = true;
141
- return operandAsObject;
142
- });
143
-
144
- const newInstr: Instruction = [specialOpCode, ...newOperands];
145
-
146
- // Preserve source-node information for error reporting
147
- (newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
148
-
149
- result.push(newInstr);
150
- }
151
-
152
- return { bytecode: result };
153
- }
@@ -1,191 +0,0 @@
1
- import * as t from "@babel/types";
2
- import traverseImport from "@babel/traverse";
3
- import { ok } from "assert";
4
- import { Compiler } from "../../compiler.ts";
5
-
6
- const traverse = (traverseImport.default ||
7
- traverseImport) as typeof traverseImport.default;
8
-
9
- // Extract the real statement list from a SwitchCase consequent.
10
- function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
11
- let stmts: t.Statement[];
12
- if (
13
- switchCase.consequent.length === 1 &&
14
- t.isBlockStatement(switchCase.consequent[0])
15
- ) {
16
- stmts = (switchCase.consequent[0] as t.BlockStatement).body;
17
- } else {
18
- stmts = switchCase.consequent as t.Statement[];
19
- }
20
- return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
21
- }
22
-
23
- // Replace every `this._operand()` call in bodyStmts with `_operands[i]`
24
- // where i is the call's sequential index (0-based).
25
- // Returns the number of replacements performed.
26
- function replaceOperandCalls(bodyStmts: t.Statement[]): number {
27
- let replaced = 0;
28
-
29
- traverse(t.blockStatement(bodyStmts), {
30
- noScope: true,
31
- CallExpression(path) {
32
- const callee = path.node.callee;
33
-
34
- const isMethodCall = (methodName) => {
35
- return (
36
- t.isMemberExpression(callee) &&
37
- t.isThisExpression(callee.object) &&
38
- t.isIdentifier(callee.property, { name: methodName }) &&
39
- path.node.arguments.length === 0
40
- );
41
- };
42
-
43
- // Replace with _operands[i]
44
- const createOperandAccess = () => {
45
- return t.memberExpression(
46
- t.identifier("_operands"),
47
- t.numericLiteral(replaced++),
48
- true, // computed
49
- );
50
- };
51
-
52
- if (isMethodCall("_operand")) {
53
- path.replaceWith(createOperandAccess());
54
- }
55
-
56
- if (isMethodCall("_constant")) {
57
- path.node.arguments = [createOperandAccess(), createOperandAccess()];
58
- }
59
- },
60
- });
61
-
62
- return replaced;
63
- }
64
-
65
- // Appends a generated switch case for every entry in compiler.ALIASED_OPS.
66
- // Each alias case:
67
- // 1. Reads all operands eagerly into `_unsortedOperands` (in the shuffled
68
- // bytecode order) via sequential this._operand() calls.
69
- // 2. Restores the original operand order into `_operands` using the INVERSE
70
- // of the stored `order` permutation:
71
- // inverseOrder[order[i]] = i
72
- // _operands[j] = _unsortedOperands[inverseOrder[j]]
73
- // This is necessary because the bytecode stored originalOperands[order[i]]
74
- // at slot i, so recovering originalOperands[j] requires the inverse lookup.
75
- // 3. Executes a clone of the original handler body where every
76
- // this._operand() has been replaced by the corresponding `_operands[i]`.
77
- //
78
- // Must run AFTER applyMacroOpcodes / applySpecializedOpcodes (so original
79
- // cases already exist) but BEFORE applyShuffleOpcodes (so the new alias
80
- // cases are also shuffled into the handler order).
81
- export function applyAliasedOpcodes(ast: t.File, compiler: Compiler): void {
82
- if (!compiler.ALIASED_OPS || Object.keys(compiler.ALIASED_OPS).length === 0)
83
- return;
84
-
85
- let switchStatement: t.SwitchStatement | null = null;
86
- traverse(ast, {
87
- SwitchStatement(path) {
88
- if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
89
- switchStatement = path.node;
90
- path.stop();
91
- }
92
- },
93
- });
94
-
95
- ok(switchStatement, "Could not find @SWITCH statement for aliased opcodes");
96
-
97
- // Build opName → SwitchCase map from existing OP.xxx case tests.
98
- const nameToCaseMap = new Map<string, t.SwitchCase>();
99
- for (const sc of (switchStatement as t.SwitchStatement).cases) {
100
- const test = sc.test;
101
- if (
102
- test &&
103
- t.isMemberExpression(test) &&
104
- t.isIdentifier(test.object, { name: "OP" }) &&
105
- t.isIdentifier(test.property)
106
- ) {
107
- nameToCaseMap.set((test.property as t.Identifier).name, sc);
108
- }
109
- }
110
-
111
- for (const [aliasOpStr, info] of Object.entries(compiler.ALIASED_OPS)) {
112
- const aliasOpCode = Number(aliasOpStr);
113
- const { originalOp, order } = info;
114
- const arity = order.length;
115
-
116
- const originalName = compiler.OP_NAME[originalOp];
117
- if (!originalName) continue;
118
-
119
- const originalCase = nameToCaseMap.get(originalName);
120
- if (!originalCase) continue;
121
-
122
- // Clone the original handler body (deep clone so we don't mutate the source)
123
- const bodyStmts: t.Statement[] = extractCaseBody(originalCase).map(
124
- (s) => t.cloneNode(s, true) as t.Statement,
125
- );
126
-
127
- // Replace this._operand() calls with _operands[i]
128
- const replaced = replaceOperandCalls(bodyStmts);
129
-
130
- // If the handler has a different number of _operand() calls than our
131
- // recorded arity, skip this alias (variable-operand handler guard).
132
- if (replaced !== arity) continue;
133
-
134
- // Build: var _unsortedOperands = [this._operand(), this._operand(), ...]
135
- // Reads operands in the NEW (shuffled) bytecode order.
136
- const unsortedInit = t.variableDeclaration("let", [
137
- t.variableDeclarator(
138
- t.identifier("_unsortedOperands"),
139
- t.arrayExpression(
140
- Array.from({ length: arity }, () =>
141
- t.callExpression(
142
- t.memberExpression(t.thisExpression(), t.identifier("_operand")),
143
- [],
144
- ),
145
- ),
146
- ),
147
- ),
148
- ]);
149
-
150
- // The inverse permutation maps original position j → unsorted index i,
151
- // because the bytecode stored originalOperands[order[i]] at slot i.
152
- // inverseOrder[j] = i means: original operand j lives at _unsortedOperands[i]
153
- const inverseOrder = new Array<number>(arity);
154
- for (let i = 0; i < arity; i++) {
155
- inverseOrder[order[i]] = i;
156
- }
157
-
158
- // Build: var _operands = [_unsortedOperands[inverseOrder[0]], ...]
159
- // Restores the original operand order expected by the handler body.
160
- const operandsInit = t.variableDeclaration("let", [
161
- t.variableDeclarator(
162
- t.identifier("_operands"),
163
- t.arrayExpression(
164
- inverseOrder.map((idx) =>
165
- t.memberExpression(
166
- t.identifier("_unsortedOperands"),
167
- t.numericLiteral(idx),
168
- true, // computed
169
- ),
170
- ),
171
- ),
172
- ),
173
- ]);
174
-
175
- const allStmts: t.Statement[] = [unsortedInit, operandsInit, ...bodyStmts];
176
-
177
- // Add a leading comment for readability in non-minified output
178
- t.addComment(
179
- allStmts[0],
180
- "leading",
181
- ` ${compiler.OP_NAME[aliasOpCode]} (order: [${order.join(",")}])`,
182
- true,
183
- );
184
-
185
- allStmts.push(t.breakStatement());
186
-
187
- (switchStatement as t.SwitchStatement).cases.push(
188
- t.switchCase(t.numericLiteral(aliasOpCode), [t.blockStatement(allStmts)]),
189
- );
190
- }
191
- }