js-confuser-vm 0.1.0 → 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.
- package/CHANGELOG.md +23 -0
- package/README.md +75 -94
- package/bench.ts +146 -0
- package/disassemble.ts +12 -0
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +134 -60
- package/dist/disassembler.js +317 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +68 -46
- package/dist/template.js +116 -0
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
- package/dist/transforms/bytecode/dispatcher.js +13 -109
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/dist/transforms/bytecode/resolveConstants.js +100 -0
- package/dist/transforms/bytecode/resolveRegisters.js +4 -0
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +18 -10
- package/dist/transforms/bytecode/stringConcealing.js +110 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +1 -1
- package/dist/utils/ast-utils.js +14 -0
- package/dist/utils/op-utils.js +0 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/index.ts +22 -17
- package/jest.config.js +14 -2
- package/output.disassembled.js +41 -0
- package/package.json +2 -1
- package/src/build-runtime.ts +113 -78
- package/src/compiler.ts +2703 -2593
- package/src/disassembler.ts +329 -0
- package/src/index.ts +12 -2
- package/src/options.ts +7 -1
- package/src/runtime.ts +84 -51
- package/src/template.ts +125 -1
- package/src/transforms/bytecode/aliasedOpcodes.ts +4 -1
- package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
- package/src/transforms/bytecode/dispatcher.ts +19 -125
- package/src/transforms/bytecode/macroOpcodes.ts +2 -2
- package/src/transforms/bytecode/resolveRegisters.ts +5 -0
- package/src/transforms/bytecode/specializedOpcodes.ts +22 -11
- package/src/transforms/bytecode/stringConcealing.ts +130 -0
- package/src/transforms/runtime/classObfuscation.ts +59 -0
- package/src/transforms/runtime/specializedOpcodes.ts +14 -9
- package/src/types.ts +42 -1
- package/src/utils/ast-utils.ts +19 -0
- package/src/utils/op-utils.ts +0 -2
- package/src/utils/pass-utils.ts +126 -0
- package/src/utils/profile-utils.ts +3 -0
- package/tsconfig.json +1 -1
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
package/dist/template.js
CHANGED
|
@@ -139,4 +139,120 @@ export class Template {
|
|
|
139
139
|
bytecode: innerBytecode
|
|
140
140
|
};
|
|
141
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
|
+
}
|
|
142
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, {
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// Control Flow Flattening (CFF)
|
|
2
|
+
//
|
|
3
|
+
// Splits each function into basic blocks and routes all execution through a
|
|
4
|
+
// while-loop + switch-style comparison chain that dispatches based on a
|
|
5
|
+
// `state` register. Original jumps become state transitions.
|
|
6
|
+
//
|
|
7
|
+
// ── How it works ─────────────────────────────────────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// 1. Each function's instruction stream is split into basic blocks at every
|
|
10
|
+
// label definition and after every terminator (JUMP, JUMP_IF_*, RETURN,
|
|
11
|
+
// THROW).
|
|
12
|
+
//
|
|
13
|
+
// 2. Each block is assigned a random u16 state value. A sentinel endState
|
|
14
|
+
// (not used by any block) marks loop termination.
|
|
15
|
+
//
|
|
16
|
+
// 3. A dispatch loop is compiled from a Template:
|
|
17
|
+
//
|
|
18
|
+
// var state = <startState>;
|
|
19
|
+
// while (state !== <endState>) {
|
|
20
|
+
// if (state === <s0>) _VM_JUMP_("<block0>");
|
|
21
|
+
// if (state === <s1>) _VM_JUMP_("<block1>");
|
|
22
|
+
// ...
|
|
23
|
+
// }
|
|
24
|
+
//
|
|
25
|
+
// The Template's `state` register is extracted via compileInline() so that
|
|
26
|
+
// block bodies can write state transitions to it.
|
|
27
|
+
//
|
|
28
|
+
// 4. Block bodies are emitted with their original instructions. Terminators
|
|
29
|
+
// are rewritten:
|
|
30
|
+
//
|
|
31
|
+
// JUMP target → LOAD_INT state, targetBlock.stateValue
|
|
32
|
+
// JUMP <loopTop>
|
|
33
|
+
//
|
|
34
|
+
// JUMP_IF_FALSE c, t → JUMP_IF_TRUE c, <skipLabel>
|
|
35
|
+
// LOAD_INT state, targetBlock.stateValue
|
|
36
|
+
// JUMP <loopTop>
|
|
37
|
+
// <skipLabel>:
|
|
38
|
+
// LOAD_INT state, fallthroughBlock.stateValue
|
|
39
|
+
// JUMP <loopTop>
|
|
40
|
+
//
|
|
41
|
+
// RETURN / THROW → kept in-place (exits the VM frame directly)
|
|
42
|
+
//
|
|
43
|
+
// 5. Block order is shuffled randomly so spatial locality gives no hints.
|
|
44
|
+
//
|
|
45
|
+
// ── Pipeline position ─────────────────────────────────────────────────────────
|
|
46
|
+
// Same slot as Dispatcher: before resolveRegisters and resolveLabels.
|
|
47
|
+
// Can run alongside Dispatcher (they are composable).
|
|
48
|
+
|
|
49
|
+
import { getRandomInt } from "../../utils/random-utils.js";
|
|
50
|
+
import { U16_MAX } from "../../utils/op-utils.js";
|
|
51
|
+
import { Template } from "../../template.js";
|
|
52
|
+
import { ref, buildMaxIdMap, forEachFunction, extractLabel } from "../../utils/pass-utils.js";
|
|
53
|
+
|
|
54
|
+
// ── Basic block splitting ────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function isTerminator(op, compiler) {
|
|
57
|
+
const OP = compiler.OP;
|
|
58
|
+
return op === OP.JUMP || op === OP.JUMP_IF_FALSE || op === OP.JUMP_IF_TRUE || op === OP.RETURN || op === OP.THROW;
|
|
59
|
+
}
|
|
60
|
+
function splitBasicBlocks(instrs, compiler) {
|
|
61
|
+
const blocks = [];
|
|
62
|
+
const usedStates = new Set();
|
|
63
|
+
const assignState = () => {
|
|
64
|
+
let s;
|
|
65
|
+
do {
|
|
66
|
+
s = getRandomInt(0, U16_MAX);
|
|
67
|
+
} while (usedStates.has(s));
|
|
68
|
+
usedStates.add(s);
|
|
69
|
+
return s;
|
|
70
|
+
};
|
|
71
|
+
let currentLabel = null;
|
|
72
|
+
let currentBody = [];
|
|
73
|
+
const flushBlock = terminator => {
|
|
74
|
+
if (currentBody.length === 0 && terminator === null && currentLabel === null) return;
|
|
75
|
+
const label = currentLabel ?? compiler._makeLabel("cff_block");
|
|
76
|
+
blocks.push({
|
|
77
|
+
label,
|
|
78
|
+
body: currentBody,
|
|
79
|
+
terminator,
|
|
80
|
+
stateValue: assignState(),
|
|
81
|
+
originalNextIndex: -1 // filled in after all blocks are created
|
|
82
|
+
});
|
|
83
|
+
currentBody = [];
|
|
84
|
+
currentLabel = null;
|
|
85
|
+
};
|
|
86
|
+
for (const instr of instrs) {
|
|
87
|
+
const op = instr[0];
|
|
88
|
+
|
|
89
|
+
// defineLabel → start a new block boundary
|
|
90
|
+
if (op === null && instr[1]?.type === "defineLabel") {
|
|
91
|
+
flushBlock(null);
|
|
92
|
+
currentLabel = instr[1].label;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Terminator → ends the current block
|
|
97
|
+
if (op !== null && isTerminator(op, compiler)) {
|
|
98
|
+
flushBlock(instr);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
currentBody.push(instr);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Flush trailing instructions
|
|
105
|
+
flushBlock(null);
|
|
106
|
+
|
|
107
|
+
// Split large blocks (> MAX_BLOCK_SIZE instructions) into smaller chunks
|
|
108
|
+
// so that no single block reveals too much sequential code.
|
|
109
|
+
const MAX_BLOCK_SIZE = 3;
|
|
110
|
+
const splitBlocks = [];
|
|
111
|
+
for (const block of blocks) {
|
|
112
|
+
if (block.body.length <= MAX_BLOCK_SIZE) {
|
|
113
|
+
splitBlocks.push(block);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Chunk the body into pieces of MAX_BLOCK_SIZE
|
|
117
|
+
for (let j = 0; j < block.body.length; j += MAX_BLOCK_SIZE) {
|
|
118
|
+
const isFirst = j === 0;
|
|
119
|
+
const isLast = j + MAX_BLOCK_SIZE >= block.body.length;
|
|
120
|
+
splitBlocks.push({
|
|
121
|
+
label: isFirst ? block.label : compiler._makeLabel("cff_split"),
|
|
122
|
+
body: block.body.slice(j, j + MAX_BLOCK_SIZE),
|
|
123
|
+
terminator: isLast ? block.terminator : null,
|
|
124
|
+
stateValue: isFirst ? block.stateValue : assignState(),
|
|
125
|
+
originalNextIndex: -1
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Replace blocks with split result
|
|
130
|
+
blocks.length = 0;
|
|
131
|
+
blocks.push(...splitBlocks);
|
|
132
|
+
|
|
133
|
+
// Wire up originalNextIndex for fallthrough resolution
|
|
134
|
+
for (let i = 0; i < blocks.length - 1; i++) {
|
|
135
|
+
blocks[i].originalNextIndex = i + 1;
|
|
136
|
+
}
|
|
137
|
+
// Last block has no successor
|
|
138
|
+
if (blocks.length > 0) {
|
|
139
|
+
blocks[blocks.length - 1].originalNextIndex = -1;
|
|
140
|
+
}
|
|
141
|
+
return blocks;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Cross-block register promotion ───────────────────────────────────────────
|
|
145
|
+
// Scans all blocks (bodies + terminators) and finds register operands that
|
|
146
|
+
// appear in more than one block. Those registers must not be in the "temp"
|
|
147
|
+
// pool because resolveRegisters' linear scan doesn't understand the CFF
|
|
148
|
+
// dispatch loop and would reuse their slots between blocks.
|
|
149
|
+
//
|
|
150
|
+
// Promotion is done in-place: we delete the `kind` property on the operand
|
|
151
|
+
// objects so they default to the "local::" pool (which never reuses slots).
|
|
152
|
+
|
|
153
|
+
function promoteMultiBlockRegisters(blocks) {
|
|
154
|
+
// (fnId, regId) → index of first block where this register was seen
|
|
155
|
+
const regFirstBlock = new Map();
|
|
156
|
+
// Set of register keys that appear in 2+ blocks
|
|
157
|
+
const multiBlockRegs = new Set();
|
|
158
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
159
|
+
const allInstrs = blocks[bi].terminator ? [...blocks[bi].body, blocks[bi].terminator] : blocks[bi].body;
|
|
160
|
+
for (const instr of allInstrs) {
|
|
161
|
+
for (let j = 1; j < instr.length; j++) {
|
|
162
|
+
const op = instr[j];
|
|
163
|
+
if (op && typeof op === "object" && op.type === "register") {
|
|
164
|
+
const key = `${op.fnId}:${op.id}`;
|
|
165
|
+
const first = regFirstBlock.get(key);
|
|
166
|
+
if (first === undefined) {
|
|
167
|
+
regFirstBlock.set(key, bi);
|
|
168
|
+
} else if (first !== bi) {
|
|
169
|
+
multiBlockRegs.add(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (multiBlockRegs.size === 0) return;
|
|
176
|
+
|
|
177
|
+
// Second pass: pin all operand instances of multi-block registers so that
|
|
178
|
+
// resolveRegisters assigns them to the "local::" pool (no slot reuse).
|
|
179
|
+
for (const block of blocks) {
|
|
180
|
+
const allInstrs = block.terminator ? [...block.body, block.terminator] : block.body;
|
|
181
|
+
for (const instr of allInstrs) {
|
|
182
|
+
for (let j = 1; j < instr.length; j++) {
|
|
183
|
+
const op = instr[j];
|
|
184
|
+
if (op && typeof op === "object" && op.type === "register") {
|
|
185
|
+
const key = `${op.fnId}:${op.id}`;
|
|
186
|
+
if (multiBlockRegs.has(key)) {
|
|
187
|
+
op.pinned = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Generate the dispatch loop via Template ──────────────────────────────────
|
|
196
|
+
|
|
197
|
+
function buildDispatchTemplate(blocks, endState, startState, compiler, fnId, maxId) {
|
|
198
|
+
// Build the if-chain cases
|
|
199
|
+
const cases = blocks.map(block => `if (state === ${block.stateValue}) _VM_JUMP_("${block.label}");`).join("\n ");
|
|
200
|
+
const source = `
|
|
201
|
+
var state = ${startState};
|
|
202
|
+
while (state !== ${endState}) {
|
|
203
|
+
${cases}
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
const tmpl = new Template(source);
|
|
207
|
+
const result = tmpl.compileInline({}, compiler, fnId, maxId);
|
|
208
|
+
|
|
209
|
+
// Pin ALL dispatch-loop registers so resolveRegisters assigns them to the
|
|
210
|
+
// "local::" pool (no slot reuse). The dispatch loop is re-entered on every
|
|
211
|
+
// state transition (backward JUMP to while_top), but the linear-scan liveness
|
|
212
|
+
// in resolveRegisters doesn't track loops and would incorrectly treat dispatch
|
|
213
|
+
// temps as dead after one pass, allowing their slots to be reused by body
|
|
214
|
+
// registers that are live across blocks.
|
|
215
|
+
for (const instr of result.bytecode) {
|
|
216
|
+
for (let j = 1; j < instr.length; j++) {
|
|
217
|
+
const op = instr[j];
|
|
218
|
+
if (op && typeof op === "object" && op.type === "register") {
|
|
219
|
+
op.pinned = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const rState = result.registers.get("state");
|
|
224
|
+
if (!rState) {
|
|
225
|
+
throw new Error("CFF: Template did not produce a 'state' register");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Find the while loop labels from the compiled IR
|
|
229
|
+
let loopTopLabel = null;
|
|
230
|
+
let loopExitLabel = null;
|
|
231
|
+
for (const instr of result.bytecode) {
|
|
232
|
+
if (instr[0] === null && instr[1]?.type === "defineLabel") {
|
|
233
|
+
const label = instr[1].label;
|
|
234
|
+
if (label.includes("while_top") && !loopTopLabel) {
|
|
235
|
+
loopTopLabel = label;
|
|
236
|
+
}
|
|
237
|
+
if (label.includes("while_exit") && !loopExitLabel) {
|
|
238
|
+
loopExitLabel = label;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!loopTopLabel || !loopExitLabel) {
|
|
243
|
+
throw new Error("CFF: Could not find while loop labels in Template output");
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
bytecode: result.bytecode,
|
|
247
|
+
rState,
|
|
248
|
+
loopTopLabel,
|
|
249
|
+
loopExitLabel,
|
|
250
|
+
innerBytecode: result.innerBytecode
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── State transition helpers ─────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
function emitStateTransition(out, rState, targetState, loopTopLabel, compiler) {
|
|
257
|
+
out.push([compiler.OP.LOAD_INT, ref(rState), targetState]);
|
|
258
|
+
out.push([compiler.OP.JUMP, {
|
|
259
|
+
type: "label",
|
|
260
|
+
label: loopTopLabel
|
|
261
|
+
}]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Per-function transformation ──────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
function processFunctionBlock(instrs, fnId, compiler, maxId) {
|
|
267
|
+
const OP = compiler.OP;
|
|
268
|
+
|
|
269
|
+
// Only transform functions that contain simple jumps
|
|
270
|
+
const hasRoutableJump = instrs.some(instr => {
|
|
271
|
+
const op = instr[0];
|
|
272
|
+
return op === OP.JUMP || op === OP.JUMP_IF_FALSE || op === OP.JUMP_IF_TRUE;
|
|
273
|
+
});
|
|
274
|
+
if (!hasRoutableJump) return {
|
|
275
|
+
instrs,
|
|
276
|
+
tail: []
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// ── 1. Split into basic blocks ──────────────────────────────────────────
|
|
280
|
+
const blocks = splitBasicBlocks(instrs, compiler);
|
|
281
|
+
if (blocks.length < 2) return {
|
|
282
|
+
instrs,
|
|
283
|
+
tail: []
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// ── 1b. Promote cross-block registers to "local" pool ──────────────────
|
|
287
|
+
// resolveRegisters does a linear-scan liveness analysis that doesn't
|
|
288
|
+
// understand the CFF dispatch loop (backward jumps). A "temp" register
|
|
289
|
+
// that's live across two blocks would appear to die within its first
|
|
290
|
+
// block and get its slot reused, corrupting values read in later blocks.
|
|
291
|
+
//
|
|
292
|
+
// Fix: find every register that appears in more than one block and
|
|
293
|
+
// delete its "temp" kind so it lands in the "local::" pool (no reuse).
|
|
294
|
+
promoteMultiBlockRegisters(blocks);
|
|
295
|
+
const usedStates = new Set(blocks.map(b => b.stateValue));
|
|
296
|
+
|
|
297
|
+
// Pick endState sentinel
|
|
298
|
+
let endState;
|
|
299
|
+
do {
|
|
300
|
+
endState = getRandomInt(0, U16_MAX);
|
|
301
|
+
} while (usedStates.has(endState));
|
|
302
|
+
const startState = blocks[0].stateValue;
|
|
303
|
+
|
|
304
|
+
// ── 2. Build dispatch loop from Template ────────────────────────────────
|
|
305
|
+
const dispatch = buildDispatchTemplate(blocks, endState, startState, compiler, fnId, maxId);
|
|
306
|
+
const {
|
|
307
|
+
rState,
|
|
308
|
+
loopTopLabel,
|
|
309
|
+
loopExitLabel
|
|
310
|
+
} = dispatch;
|
|
311
|
+
|
|
312
|
+
// ── 3. Pre-compute all state mappings BEFORE shuffle ─────────────────
|
|
313
|
+
// These maps capture the correct stateValues while the blocks array is
|
|
314
|
+
// still in its original split order. After the shuffle, indexing into
|
|
315
|
+
// blocks[] by original index would give the wrong block.
|
|
316
|
+
|
|
317
|
+
// label → stateValue (for jump target resolution)
|
|
318
|
+
const labelToState = new Map();
|
|
319
|
+
for (const block of blocks) {
|
|
320
|
+
labelToState.set(block.label, block.stateValue);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// originalIndex → fallthrough stateValue
|
|
324
|
+
const fallthroughStateMap = new Map();
|
|
325
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
326
|
+
const next = blocks[i].originalNextIndex;
|
|
327
|
+
fallthroughStateMap.set(i, next >= 0 ? blocks[next].stateValue : endState);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── 4. Shuffle block order ──────────────────────────────────────────────
|
|
331
|
+
// Track which original index each shuffled position came from, so we can
|
|
332
|
+
// look up fallthroughStateMap correctly during emission.
|
|
333
|
+
const originalIndices = blocks.map((_, i) => i);
|
|
334
|
+
|
|
335
|
+
// Fisher-Yates shuffle
|
|
336
|
+
for (let i = blocks.length - 1; i > 0; i--) {
|
|
337
|
+
const j = getRandomInt(0, i);
|
|
338
|
+
[blocks[i], blocks[j]] = [blocks[j], blocks[i]];
|
|
339
|
+
[originalIndices[i], originalIndices[j]] = [originalIndices[j], originalIndices[i]];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── 5. Emit: dispatch loop + block bodies ───────────────────────────────
|
|
343
|
+
const out = [];
|
|
344
|
+
|
|
345
|
+
// Dispatch loop (var state = ...; while(...) { if-chain })
|
|
346
|
+
out.push(...dispatch.bytecode);
|
|
347
|
+
|
|
348
|
+
// Each block: defineLabel → body → state transition → JUMP loopTop
|
|
349
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
350
|
+
const block = blocks[i];
|
|
351
|
+
const origIdx = originalIndices[i];
|
|
352
|
+
|
|
353
|
+
// Block label
|
|
354
|
+
out.push([null, {
|
|
355
|
+
type: "defineLabel",
|
|
356
|
+
label: block.label
|
|
357
|
+
}]);
|
|
358
|
+
|
|
359
|
+
// Block body
|
|
360
|
+
out.push(...block.body);
|
|
361
|
+
|
|
362
|
+
// Terminator rewriting
|
|
363
|
+
const term = block.terminator;
|
|
364
|
+
if (term === null) {
|
|
365
|
+
// Fallthrough → transition to the original next block's state
|
|
366
|
+
emitStateTransition(out, rState, fallthroughStateMap.get(origIdx), loopTopLabel, compiler);
|
|
367
|
+
} else if (term[0] === OP.RETURN || term[0] === OP.THROW) {
|
|
368
|
+
// Exits the frame — emit as-is
|
|
369
|
+
out.push(term);
|
|
370
|
+
} else if (term[0] === OP.JUMP) {
|
|
371
|
+
const targetLabel = extractLabel(term[1]);
|
|
372
|
+
if (targetLabel !== null) {
|
|
373
|
+
const targetState = labelToState.get(targetLabel);
|
|
374
|
+
if (targetState !== undefined) {
|
|
375
|
+
emitStateTransition(out, rState, targetState, loopTopLabel, compiler);
|
|
376
|
+
} else {
|
|
377
|
+
// Target outside this function's blocks — keep original
|
|
378
|
+
out.push(term);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
out.push(term);
|
|
382
|
+
}
|
|
383
|
+
} else if (term[0] === OP.JUMP_IF_FALSE) {
|
|
384
|
+
// Original: if (!cond) goto target; else fallthrough
|
|
385
|
+
// → if (cond) goto skipLabel (inverted)
|
|
386
|
+
// state = targetState; goto loopTop
|
|
387
|
+
// skipLabel:
|
|
388
|
+
// state = fallthroughState; goto loopTop
|
|
389
|
+
const cond = term[1];
|
|
390
|
+
const targetLabel = extractLabel(term[2]);
|
|
391
|
+
if (targetLabel !== null) {
|
|
392
|
+
const targetState = labelToState.get(targetLabel);
|
|
393
|
+
if (targetState !== undefined) {
|
|
394
|
+
const skipLabel = compiler._makeLabel("cff_skip");
|
|
395
|
+
out.push([OP.JUMP_IF_TRUE, cond, {
|
|
396
|
+
type: "label",
|
|
397
|
+
label: skipLabel
|
|
398
|
+
}]);
|
|
399
|
+
emitStateTransition(out, rState, targetState, loopTopLabel, compiler);
|
|
400
|
+
out.push([null, {
|
|
401
|
+
type: "defineLabel",
|
|
402
|
+
label: skipLabel
|
|
403
|
+
}]);
|
|
404
|
+
emitStateTransition(out, rState, fallthroughStateMap.get(origIdx), loopTopLabel, compiler);
|
|
405
|
+
} else {
|
|
406
|
+
out.push(term);
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
out.push(term);
|
|
410
|
+
}
|
|
411
|
+
} else if (term[0] === OP.JUMP_IF_TRUE) {
|
|
412
|
+
// Original: if (cond) goto target; else fallthrough
|
|
413
|
+
// → if (!cond) goto skipLabel (inverted)
|
|
414
|
+
// state = targetState; goto loopTop
|
|
415
|
+
// skipLabel:
|
|
416
|
+
// state = fallthroughState; goto loopTop
|
|
417
|
+
const cond = term[1];
|
|
418
|
+
const targetLabel = extractLabel(term[2]);
|
|
419
|
+
if (targetLabel !== null) {
|
|
420
|
+
const targetState = labelToState.get(targetLabel);
|
|
421
|
+
if (targetState !== undefined) {
|
|
422
|
+
const skipLabel = compiler._makeLabel("cff_skip");
|
|
423
|
+
out.push([OP.JUMP_IF_FALSE, cond, {
|
|
424
|
+
type: "label",
|
|
425
|
+
label: skipLabel
|
|
426
|
+
}]);
|
|
427
|
+
emitStateTransition(out, rState, targetState, loopTopLabel, compiler);
|
|
428
|
+
out.push([null, {
|
|
429
|
+
type: "defineLabel",
|
|
430
|
+
label: skipLabel
|
|
431
|
+
}]);
|
|
432
|
+
emitStateTransition(out, rState, fallthroughStateMap.get(origIdx), loopTopLabel, compiler);
|
|
433
|
+
} else {
|
|
434
|
+
out.push(term);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
out.push(term);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
instrs: out,
|
|
443
|
+
tail: dispatch.innerBytecode
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Pass entry point ──────────────────────────────────────────────────────────
|
|
448
|
+
export function controlFlowFlattening(bc, compiler) {
|
|
449
|
+
const maxId = buildMaxIdMap(bc);
|
|
450
|
+
return forEachFunction(bc, compiler, (fnInstrs, fnId) => processFunctionBlock(fnInstrs, fnId, compiler, maxId));
|
|
451
|
+
}
|