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.
- package/README.md +281 -147
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +714 -265
- package/dist/disassembler.js +367 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +160 -119
- package/dist/template.js +163 -42
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/concealConstants.js +2 -2
- package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
- package/dist/transforms/bytecode/dispatcher.js +15 -111
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
- package/dist/transforms/bytecode/resolveRegisters.js +23 -4
- package/dist/transforms/bytecode/selfModifying.js +88 -21
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
- package/dist/transforms/bytecode/stringConcealing.js +288 -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 +75 -0
- package/dist/utils/op-utils.js +1 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/package.json +8 -1
- package/.gitmodules +0 -4
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -335
- package/babel-plugin-inline-runtime.cjs +0 -34
- package/babel.config.json +0 -23
- package/index.ts +0 -38
- package/jest-strip-types.js +0 -10
- package/jest.config.js +0 -52
- package/src/build-runtime.ts +0 -78
- package/src/compiler.ts +0 -2593
- package/src/index.ts +0 -14
- package/src/minify.ts +0 -21
- package/src/options.ts +0 -18
- package/src/runtime.ts +0 -923
- package/src/template.ts +0 -141
- package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
- package/src/transforms/bytecode/concealConstants.ts +0 -52
- package/src/transforms/bytecode/dispatcher.ts +0 -398
- package/src/transforms/bytecode/macroOpcodes.ts +0 -193
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/bytecode/resolveLabels.ts +0 -112
- package/src/transforms/bytecode/resolveRegisters.ts +0 -221
- package/src/transforms/bytecode/selfModifying.ts +0 -121
- package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
- package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/macroOpcodes.ts +0 -138
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- package/src/transforms/runtime/minify.ts +0 -1
- package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
- package/src/transforms/runtime/specializedOpcodes.ts +0 -156
- package/src/types.ts +0 -93
- package/src/utils/op-utils.ts +0 -48
- package/src/utils/random-utils.ts +0 -31
- package/tsconfig.json +0 -12
|
@@ -0,0 +1,569 @@
|
|
|
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. Rather than comparing the
|
|
17
|
+
// state register against an absolute constant in each arm, a single
|
|
18
|
+
// accumulator `c` walks the (ascending-sorted) state values RELATIVELY:
|
|
19
|
+
// it is seeded with the smallest state at the top of every iteration and
|
|
20
|
+
// each subsequent arm adds the delta to the previous state, so the target
|
|
21
|
+
// state of an arm is `oldState + diff` rather than a readable literal.
|
|
22
|
+
//
|
|
23
|
+
// var state = <startState>;
|
|
24
|
+
// var c = 0;
|
|
25
|
+
// while (state !== <endState>) {
|
|
26
|
+
// c = <s0>; if (state === c) _VM_JUMP_("<block0>");
|
|
27
|
+
// c += <s1 - s0>; if (state === c) _VM_JUMP_("<block1>");
|
|
28
|
+
// c -= <s1 - s2>; if (state === c) _VM_JUMP_("<block2>");
|
|
29
|
+
// ...
|
|
30
|
+
// }
|
|
31
|
+
//
|
|
32
|
+
// The running sum telescopes (c = s0 + Σ(si − si−1) = si exactly), so chain
|
|
33
|
+
// order is irrelevant to correctness and is shuffled unpredictably. Deltas
|
|
34
|
+
// may be negative; since LOAD_INT operands are unsigned u16 a negative delta
|
|
35
|
+
// is emitted as a `-=` of its magnitude (always <= U16_MAX) rather than via
|
|
36
|
+
// masking. Static solvers can no longer read which block a state routes to
|
|
37
|
+
// without replaying the running sum.
|
|
38
|
+
//
|
|
39
|
+
// The Template's `state` register is extracted via compileInline() so that
|
|
40
|
+
// block bodies can write state transitions to it.
|
|
41
|
+
//
|
|
42
|
+
// 4. Block bodies are emitted with their original instructions. Terminators
|
|
43
|
+
// are rewritten. Each transition is RELATIVE: when a block runs, the state
|
|
44
|
+
// register still holds that block's own dispatch value, so the target is
|
|
45
|
+
// reached by ADDing the delta (target − current) rather than loading the
|
|
46
|
+
// absolute next-state as a constant. A negative delta is a SUB of its
|
|
47
|
+
// magnitude (additive operators only — no constant, no masking):
|
|
48
|
+
//
|
|
49
|
+
// JUMP target → LOAD_INT delta, |targetState - blockState|
|
|
50
|
+
// ADD/SUB state, state, delta
|
|
51
|
+
// JUMP <loopTop>
|
|
52
|
+
//
|
|
53
|
+
// JUMP_IF_FALSE c, t → JUMP_IF_TRUE c, <skipLabel>
|
|
54
|
+
// ADD/SUB state, state, <delta to targetState>
|
|
55
|
+
// JUMP <loopTop>
|
|
56
|
+
// <skipLabel>:
|
|
57
|
+
// ADD/SUB state, state, <delta to fallthrough>
|
|
58
|
+
// JUMP <loopTop>
|
|
59
|
+
//
|
|
60
|
+
// RETURN / THROW → kept in-place (exits the VM frame directly)
|
|
61
|
+
//
|
|
62
|
+
// Relative transitions assume the `state` register holds the running block's
|
|
63
|
+
// own value on entry — true for every dispatcher-routed entry. Some opcodes
|
|
64
|
+
// (FOR_IN_NEXT, TRY_SETUP, FINALLY_SETUP, and JUMP_REG via LOAD_INT-of-label)
|
|
65
|
+
// jump DIRECTLY to a block label, bypassing the dispatcher, so for those
|
|
66
|
+
// "direct-entry" blocks `state` is seeded absolutely at entry before the
|
|
67
|
+
// relative math runs (see collectDirectEntryLabels).
|
|
68
|
+
//
|
|
69
|
+
// 5. Block order is shuffled randomly so spatial locality gives no hints.
|
|
70
|
+
//
|
|
71
|
+
// ── Pipeline position ─────────────────────────────────────────────────────────
|
|
72
|
+
// Same slot as Dispatcher: before resolveRegisters and resolveLabels.
|
|
73
|
+
// Can run alongside Dispatcher (they are composable).
|
|
74
|
+
|
|
75
|
+
import { getRandomInt } from "../../utils/random-utils.js";
|
|
76
|
+
import { U16_MAX } from "../../utils/op-utils.js";
|
|
77
|
+
import { Template } from "../../template.js";
|
|
78
|
+
import { ref, allocReg, buildMaxIdMap, forEachFunction, extractLabel } from "../../utils/pass-utils.js";
|
|
79
|
+
|
|
80
|
+
// ── Basic block splitting ────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function isTerminator(op, compiler) {
|
|
83
|
+
const OP = compiler.OP;
|
|
84
|
+
return op === OP.JUMP || op === OP.JUMP_IF_FALSE || op === OP.JUMP_IF_TRUE || op === OP.RETURN || op === OP.THROW;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Direct-entry block detection ─────────────────────────────────────────────
|
|
88
|
+
// CFF rewrites the JUMP / JUMP_IF_* terminators into state transitions that all
|
|
89
|
+
// route through the dispatch loop, so a block entered that way always has the
|
|
90
|
+
// dispatcher's matched value in `state`. But several opcodes embed a target
|
|
91
|
+
// label and jump to it DIRECTLY, bypassing the dispatch loop entirely:
|
|
92
|
+
//
|
|
93
|
+
// • FOR_IN_NEXT exitTarget (loop-done jump)
|
|
94
|
+
// • TRY_SETUP handlerPc (catch entry, taken by the VM unwinder)
|
|
95
|
+
// • FINALLY_SETUP finallyPc / throwPad (finalizer + re-raise pad)
|
|
96
|
+
// • LOAD_INT reg, <label> → JUMP_REG (finally continuation / break / continue
|
|
97
|
+
// resume pads materialized by _emitLoadLabel)
|
|
98
|
+
//
|
|
99
|
+
// A block reached through one of these does NOT have its own stateValue in the
|
|
100
|
+
// `state` register, which breaks the RELATIVE transition (it assumes state ==
|
|
101
|
+
// blockState on entry). We collect every label referenced by a NON-terminator
|
|
102
|
+
// instruction; the blocks owning those labels are seeded with an absolute
|
|
103
|
+
// `state = blockState` at entry so the relative terminator math stays correct.
|
|
104
|
+
function collectDirectEntryLabels(instrs, compiler) {
|
|
105
|
+
const labels = new Set();
|
|
106
|
+
for (const instr of instrs) {
|
|
107
|
+
const op = instr[0];
|
|
108
|
+
if (op === null) continue; // IR pseudo (defineLabel) — not a real jump
|
|
109
|
+
if (isTerminator(op, compiler)) continue; // rewritten → routed through dispatcher
|
|
110
|
+
for (let j = 1; j < instr.length; j++) {
|
|
111
|
+
const operand = instr[j];
|
|
112
|
+
if (operand && typeof operand === "object" && operand.type === "label") {
|
|
113
|
+
labels.add(operand.label);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return labels;
|
|
118
|
+
}
|
|
119
|
+
function splitBasicBlocks(instrs, compiler) {
|
|
120
|
+
const blocks = [];
|
|
121
|
+
const usedStates = new Set();
|
|
122
|
+
const assignState = () => {
|
|
123
|
+
let s;
|
|
124
|
+
do {
|
|
125
|
+
s = getRandomInt(0, U16_MAX);
|
|
126
|
+
} while (usedStates.has(s));
|
|
127
|
+
usedStates.add(s);
|
|
128
|
+
return s;
|
|
129
|
+
};
|
|
130
|
+
let currentLabel = null;
|
|
131
|
+
let currentBody = [];
|
|
132
|
+
const flushBlock = terminator => {
|
|
133
|
+
if (currentBody.length === 0 && terminator === null && currentLabel === null) return;
|
|
134
|
+
const label = currentLabel ?? compiler._makeLabel("cff_block");
|
|
135
|
+
blocks.push({
|
|
136
|
+
label,
|
|
137
|
+
body: currentBody,
|
|
138
|
+
terminator,
|
|
139
|
+
stateValue: assignState(),
|
|
140
|
+
originalNextIndex: -1 // filled in after all blocks are created
|
|
141
|
+
});
|
|
142
|
+
currentBody = [];
|
|
143
|
+
currentLabel = null;
|
|
144
|
+
};
|
|
145
|
+
for (const instr of instrs) {
|
|
146
|
+
const op = instr[0];
|
|
147
|
+
|
|
148
|
+
// defineLabel → start a new block boundary
|
|
149
|
+
if (op === null && instr[1]?.type === "defineLabel") {
|
|
150
|
+
flushBlock(null);
|
|
151
|
+
currentLabel = instr[1].label;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Terminator → ends the current block
|
|
156
|
+
if (op !== null && isTerminator(op, compiler)) {
|
|
157
|
+
flushBlock(instr);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
currentBody.push(instr);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Flush trailing instructions
|
|
164
|
+
flushBlock(null);
|
|
165
|
+
|
|
166
|
+
// Split large blocks (> MAX_BLOCK_SIZE instructions) into smaller chunks
|
|
167
|
+
// so that no single block reveals too much sequential code.
|
|
168
|
+
const MAX_BLOCK_SIZE = 3;
|
|
169
|
+
const splitBlocks = [];
|
|
170
|
+
for (const block of blocks) {
|
|
171
|
+
if (block.body.length <= MAX_BLOCK_SIZE) {
|
|
172
|
+
splitBlocks.push(block);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Chunk the body into pieces of MAX_BLOCK_SIZE
|
|
176
|
+
for (let j = 0; j < block.body.length; j += MAX_BLOCK_SIZE) {
|
|
177
|
+
const isFirst = j === 0;
|
|
178
|
+
const isLast = j + MAX_BLOCK_SIZE >= block.body.length;
|
|
179
|
+
splitBlocks.push({
|
|
180
|
+
label: isFirst ? block.label : compiler._makeLabel("cff_split"),
|
|
181
|
+
body: block.body.slice(j, j + MAX_BLOCK_SIZE),
|
|
182
|
+
terminator: isLast ? block.terminator : null,
|
|
183
|
+
stateValue: isFirst ? block.stateValue : assignState(),
|
|
184
|
+
originalNextIndex: -1
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Replace blocks with split result
|
|
189
|
+
blocks.length = 0;
|
|
190
|
+
blocks.push(...splitBlocks);
|
|
191
|
+
|
|
192
|
+
// Wire up originalNextIndex for fallthrough resolution
|
|
193
|
+
for (let i = 0; i < blocks.length - 1; i++) {
|
|
194
|
+
blocks[i].originalNextIndex = i + 1;
|
|
195
|
+
}
|
|
196
|
+
// Last block has no successor
|
|
197
|
+
if (blocks.length > 0) {
|
|
198
|
+
blocks[blocks.length - 1].originalNextIndex = -1;
|
|
199
|
+
}
|
|
200
|
+
return blocks;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Cross-block register promotion ───────────────────────────────────────────
|
|
204
|
+
// Scans all blocks (bodies + terminators) and finds register operands that
|
|
205
|
+
// appear in more than one block. Those registers must not be in the "temp"
|
|
206
|
+
// pool because resolveRegisters' linear scan doesn't understand the CFF
|
|
207
|
+
// dispatch loop and would reuse their slots between blocks.
|
|
208
|
+
//
|
|
209
|
+
// Promotion is done in-place: we delete the `kind` property on the operand
|
|
210
|
+
// objects so they default to the "local::" pool (which never reuses slots).
|
|
211
|
+
|
|
212
|
+
function promoteMultiBlockRegisters(blocks) {
|
|
213
|
+
// (fnId, regId) → index of first block where this register was seen
|
|
214
|
+
const regFirstBlock = new Map();
|
|
215
|
+
// Set of register keys that appear in 2+ blocks
|
|
216
|
+
const multiBlockRegs = new Set();
|
|
217
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
218
|
+
const allInstrs = blocks[bi].terminator ? [...blocks[bi].body, blocks[bi].terminator] : blocks[bi].body;
|
|
219
|
+
for (const instr of allInstrs) {
|
|
220
|
+
for (let j = 1; j < instr.length; j++) {
|
|
221
|
+
const op = instr[j];
|
|
222
|
+
if (op && typeof op === "object" && op.type === "register") {
|
|
223
|
+
const key = `${op.fnId}:${op.id}`;
|
|
224
|
+
const first = regFirstBlock.get(key);
|
|
225
|
+
if (first === undefined) {
|
|
226
|
+
regFirstBlock.set(key, bi);
|
|
227
|
+
} else if (first !== bi) {
|
|
228
|
+
multiBlockRegs.add(key);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (multiBlockRegs.size === 0) return;
|
|
235
|
+
|
|
236
|
+
// Second pass: pin all operand instances of multi-block registers so that
|
|
237
|
+
// resolveRegisters assigns them to the "local::" pool (no slot reuse).
|
|
238
|
+
for (const block of blocks) {
|
|
239
|
+
const allInstrs = block.terminator ? [...block.body, block.terminator] : block.body;
|
|
240
|
+
for (const instr of allInstrs) {
|
|
241
|
+
for (let j = 1; j < instr.length; j++) {
|
|
242
|
+
const op = instr[j];
|
|
243
|
+
if (op && typeof op === "object" && op.type === "register") {
|
|
244
|
+
const key = `${op.fnId}:${op.id}`;
|
|
245
|
+
if (multiBlockRegs.has(key)) {
|
|
246
|
+
op.pinned = true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Generate the dispatch loop via Template ──────────────────────────────────
|
|
255
|
+
|
|
256
|
+
function buildDispatchTemplate(blocks, endState, startState, compiler, fnId, maxId) {
|
|
257
|
+
// Build the if-chain using a RELATIVE comparison accumulator.
|
|
258
|
+
//
|
|
259
|
+
// The accumulator `c` is seeded with the first arm's state at the top of each
|
|
260
|
+
// iteration, then each subsequent arm adjusts it by the delta from the
|
|
261
|
+
// previous state (new = oldState + diff). Because the running sum telescopes
|
|
262
|
+
// (c = s0 + Σ(si − si−1) = si exactly), the chain order is irrelevant to
|
|
263
|
+
// correctness, so we shuffle it into an unpredictable order. Deltas can be
|
|
264
|
+
// negative; since LOAD_INT operands are unsigned u16 we emit a `-=` of the
|
|
265
|
+
// magnitude in that case rather than masking — every magnitude is <= U16_MAX.
|
|
266
|
+
const chainOrder = [...blocks];
|
|
267
|
+
for (let i = chainOrder.length - 1; i > 0; i--) {
|
|
268
|
+
const j = getRandomInt(0, i);
|
|
269
|
+
[chainOrder[i], chainOrder[j]] = [chainOrder[j], chainOrder[i]];
|
|
270
|
+
}
|
|
271
|
+
const cases = [];
|
|
272
|
+
let prevState = chainOrder[0].stateValue;
|
|
273
|
+
cases.push(`c = ${prevState};`);
|
|
274
|
+
cases.push(`if (state === c) _VM_JUMP_("${chainOrder[0].label}");`);
|
|
275
|
+
for (let i = 1; i < chainOrder.length; i++) {
|
|
276
|
+
const delta = chainOrder[i].stateValue - prevState;
|
|
277
|
+
cases.push(delta >= 0 ? `c += ${delta};` : `c -= ${-delta};`);
|
|
278
|
+
cases.push(`if (state === c) _VM_JUMP_("${chainOrder[i].label}");`);
|
|
279
|
+
prevState = chainOrder[i].stateValue;
|
|
280
|
+
}
|
|
281
|
+
const source = `
|
|
282
|
+
var state = ${startState};
|
|
283
|
+
var c = 0;
|
|
284
|
+
while (state !== ${endState}) {
|
|
285
|
+
${cases.join("\n ")}
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
const template = new Template(source);
|
|
289
|
+
const result = template.compileInline({}, compiler, fnId, maxId);
|
|
290
|
+
|
|
291
|
+
// Pin ALL dispatch-loop registers so resolveRegisters assigns them to the
|
|
292
|
+
// "local::" pool (no slot reuse). The dispatch loop is re-entered on every
|
|
293
|
+
// state transition (backward JUMP to while_top), but the linear-scan liveness
|
|
294
|
+
// in resolveRegisters doesn't track loops and would incorrectly treat dispatch
|
|
295
|
+
// temps as dead after one pass, allowing their slots to be reused by body
|
|
296
|
+
// registers that are live across blocks.
|
|
297
|
+
for (const instr of result.bytecode) {
|
|
298
|
+
for (let j = 1; j < instr.length; j++) {
|
|
299
|
+
const op = instr[j];
|
|
300
|
+
if (op && typeof op === "object" && op.type === "register") {
|
|
301
|
+
op.pinned = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const rState = result.registers.get("state");
|
|
306
|
+
if (!rState) {
|
|
307
|
+
throw new Error("CFF: Template did not produce a 'state' register");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Find the while loop labels from the compiled IR
|
|
311
|
+
let loopTopLabel = null;
|
|
312
|
+
let loopExitLabel = null;
|
|
313
|
+
for (const instr of result.bytecode) {
|
|
314
|
+
if (instr[0] === null && instr[1]?.type === "defineLabel") {
|
|
315
|
+
const label = instr[1].label;
|
|
316
|
+
if (label.includes("while_top") && !loopTopLabel) {
|
|
317
|
+
loopTopLabel = label;
|
|
318
|
+
}
|
|
319
|
+
if (label.includes("while_exit") && !loopExitLabel) {
|
|
320
|
+
loopExitLabel = label;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!loopTopLabel || !loopExitLabel) {
|
|
325
|
+
throw new Error("CFF: Could not find while loop labels in Template output");
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
bytecode: result.bytecode,
|
|
329
|
+
rState,
|
|
330
|
+
loopTopLabel,
|
|
331
|
+
loopExitLabel,
|
|
332
|
+
innerBytecode: result.innerBytecode
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── State transition helpers ─────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
// Emit a RELATIVE state transition. When a block runs, the state register
|
|
339
|
+
// still holds that block's own dispatch value (`currentState`), so adjusting it
|
|
340
|
+
// by the delta lands exactly on `targetState` — without ever loading the
|
|
341
|
+
// absolute next-state as a constant, which is what static solvers read to lift
|
|
342
|
+
// the CFG. The delta is applied with additive operators only: a non-negative
|
|
343
|
+
// delta is an ADD, a negative one a SUB of its magnitude (so the loaded operand
|
|
344
|
+
// always stays within the unsigned u16 range LOAD_INT requires — no masking).
|
|
345
|
+
function emitStateTransition(out, rState, rDelta, currentState, targetState, loopTopLabel, compiler) {
|
|
346
|
+
const OP = compiler.OP;
|
|
347
|
+
const delta = targetState - currentState;
|
|
348
|
+
out.push([OP.LOAD_INT, ref(rDelta), Math.abs(delta)]);
|
|
349
|
+
out.push([delta >= 0 ? OP.ADD : OP.SUB, ref(rState), ref(rState), ref(rDelta)]);
|
|
350
|
+
out.push([OP.JUMP, {
|
|
351
|
+
type: "label",
|
|
352
|
+
label: loopTopLabel
|
|
353
|
+
}]);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Per-function transformation ──────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
function processFunctionBlock(instrs, fnId, compiler, maxId) {
|
|
359
|
+
const OP = compiler.OP;
|
|
360
|
+
|
|
361
|
+
// Only transform functions that contain simple jumps
|
|
362
|
+
const hasRoutableJump = instrs.some(instr => {
|
|
363
|
+
const op = instr[0];
|
|
364
|
+
return op === OP.JUMP || op === OP.JUMP_IF_FALSE || op === OP.JUMP_IF_TRUE;
|
|
365
|
+
});
|
|
366
|
+
if (!hasRoutableJump) return {
|
|
367
|
+
instrs,
|
|
368
|
+
tail: []
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Labels that can be entered by an embedded/indirect jump (FOR_IN_NEXT exit,
|
|
372
|
+
// catch/finally handlers, JUMP_REG continuation pads) — collected from the
|
|
373
|
+
// ORIGINAL stream before it is carved into blocks. Blocks owning these labels
|
|
374
|
+
// need an absolute state seed (see emission below) because the RELATIVE
|
|
375
|
+
// transition assumes `state` already holds the block's value on entry, which
|
|
376
|
+
// only holds for dispatcher-routed entries.
|
|
377
|
+
const directEntryLabels = collectDirectEntryLabels(instrs, compiler);
|
|
378
|
+
|
|
379
|
+
// ── 1. Split into basic blocks ──────────────────────────────────────────
|
|
380
|
+
const blocks = splitBasicBlocks(instrs, compiler);
|
|
381
|
+
if (blocks.length < 2) return {
|
|
382
|
+
instrs,
|
|
383
|
+
tail: []
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// ── 1b. Promote cross-block registers to "local" pool ──────────────────
|
|
387
|
+
// resolveRegisters does a linear-scan liveness analysis that doesn't
|
|
388
|
+
// understand the CFF dispatch loop (backward jumps). A "temp" register
|
|
389
|
+
// that's live across two blocks would appear to die within its first
|
|
390
|
+
// block and get its slot reused, corrupting values read in later blocks.
|
|
391
|
+
//
|
|
392
|
+
// Fix: find every register that appears in more than one block and
|
|
393
|
+
// delete its "temp" kind so it lands in the "local::" pool (no reuse).
|
|
394
|
+
promoteMultiBlockRegisters(blocks);
|
|
395
|
+
const usedStates = new Set(blocks.map(b => b.stateValue));
|
|
396
|
+
|
|
397
|
+
// Pick endState sentinel
|
|
398
|
+
let endState;
|
|
399
|
+
do {
|
|
400
|
+
endState = getRandomInt(0, U16_MAX);
|
|
401
|
+
} while (usedStates.has(endState));
|
|
402
|
+
const startState = blocks[0].stateValue;
|
|
403
|
+
|
|
404
|
+
// ── 2. Build dispatch loop from Template ────────────────────────────────
|
|
405
|
+
const dispatch = buildDispatchTemplate(blocks, endState, startState, compiler, fnId, maxId);
|
|
406
|
+
const {
|
|
407
|
+
rState,
|
|
408
|
+
loopTopLabel,
|
|
409
|
+
loopExitLabel
|
|
410
|
+
} = dispatch;
|
|
411
|
+
|
|
412
|
+
// Scratch register holding the per-transition delta. allocReg yields a
|
|
413
|
+
// "local::" register (the same pool pinned dispatch registers use), so it is
|
|
414
|
+
// never slot-reused across blocks. It is rewritten before every use, so a
|
|
415
|
+
// single register is safely shared by all transitions.
|
|
416
|
+
const rDelta = allocReg(fnId, maxId);
|
|
417
|
+
|
|
418
|
+
// ── 3. Pre-compute all state mappings BEFORE shuffle ─────────────────
|
|
419
|
+
// These maps capture the correct stateValues while the blocks array is
|
|
420
|
+
// still in its original split order. After the shuffle, indexing into
|
|
421
|
+
// blocks[] by original index would give the wrong block.
|
|
422
|
+
|
|
423
|
+
// label → stateValue (for jump target resolution)
|
|
424
|
+
const labelToState = new Map();
|
|
425
|
+
for (const block of blocks) {
|
|
426
|
+
labelToState.set(block.label, block.stateValue);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// originalIndex → fallthrough stateValue
|
|
430
|
+
const fallthroughStateMap = new Map();
|
|
431
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
432
|
+
const next = blocks[i].originalNextIndex;
|
|
433
|
+
fallthroughStateMap.set(i, next >= 0 ? blocks[next].stateValue : endState);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── 4. Shuffle block order ──────────────────────────────────────────────
|
|
437
|
+
// Track which original index each shuffled position came from, so we can
|
|
438
|
+
// look up fallthroughStateMap correctly during emission.
|
|
439
|
+
const originalIndices = blocks.map((_, i) => i);
|
|
440
|
+
|
|
441
|
+
// Fisher-Yates shuffle
|
|
442
|
+
for (let i = blocks.length - 1; i > 0; i--) {
|
|
443
|
+
const j = getRandomInt(0, i);
|
|
444
|
+
[blocks[i], blocks[j]] = [blocks[j], blocks[i]];
|
|
445
|
+
[originalIndices[i], originalIndices[j]] = [originalIndices[j], originalIndices[i]];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── 5. Emit: dispatch loop + block bodies ───────────────────────────────
|
|
449
|
+
const out = [];
|
|
450
|
+
|
|
451
|
+
// Dispatch loop (var state = ...; while(...) { if-chain })
|
|
452
|
+
out.push(...dispatch.bytecode);
|
|
453
|
+
|
|
454
|
+
// Each block: defineLabel → body → state transition → JUMP loopTop
|
|
455
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
456
|
+
const block = blocks[i];
|
|
457
|
+
const origIdx = originalIndices[i];
|
|
458
|
+
|
|
459
|
+
// Block label
|
|
460
|
+
out.push([null, {
|
|
461
|
+
type: "defineLabel",
|
|
462
|
+
label: block.label
|
|
463
|
+
}]);
|
|
464
|
+
|
|
465
|
+
// If this block can be entered by a jump that bypasses the dispatch loop
|
|
466
|
+
// (FOR_IN_NEXT exit, catch/finally handlers, JUMP_REG continuation pads),
|
|
467
|
+
// the `state` register may not hold this block's value on entry. Seed it
|
|
468
|
+
// absolutely so the relative terminator transition below lands correctly.
|
|
469
|
+
// (split keeps the original label on the first sub-block, which is exactly
|
|
470
|
+
// the jump target, so seeding it is sufficient.) When the block is instead
|
|
471
|
+
// reached through the dispatcher, state already equals blockState and this
|
|
472
|
+
// write is a harmless no-op.
|
|
473
|
+
if (directEntryLabels.has(block.label)) {
|
|
474
|
+
out.push([OP.LOAD_INT, ref(rState), block.stateValue]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Block body
|
|
478
|
+
out.push(...block.body);
|
|
479
|
+
|
|
480
|
+
// Terminator rewriting
|
|
481
|
+
const term = block.terminator;
|
|
482
|
+
if (term === null) {
|
|
483
|
+
// Fallthrough → transition to the original next block's state
|
|
484
|
+
emitStateTransition(out, rState, rDelta, block.stateValue, fallthroughStateMap.get(origIdx), loopTopLabel, compiler);
|
|
485
|
+
} else if (term[0] === OP.RETURN || term[0] === OP.THROW) {
|
|
486
|
+
// Exits the frame — emit as-is
|
|
487
|
+
out.push(term);
|
|
488
|
+
} else if (term[0] === OP.JUMP) {
|
|
489
|
+
const targetLabel = extractLabel(term[1]);
|
|
490
|
+
if (targetLabel !== null) {
|
|
491
|
+
const targetState = labelToState.get(targetLabel);
|
|
492
|
+
if (targetState !== undefined) {
|
|
493
|
+
emitStateTransition(out, rState, rDelta, block.stateValue, targetState, loopTopLabel, compiler);
|
|
494
|
+
} else {
|
|
495
|
+
// Target outside this function's blocks — keep original
|
|
496
|
+
out.push(term);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
out.push(term);
|
|
500
|
+
}
|
|
501
|
+
} else if (term[0] === OP.JUMP_IF_FALSE) {
|
|
502
|
+
// Original: if (!cond) goto target; else fallthrough
|
|
503
|
+
// → if (cond) goto skipLabel (inverted)
|
|
504
|
+
// state = targetState; goto loopTop
|
|
505
|
+
// skipLabel:
|
|
506
|
+
// state = fallthroughState; goto loopTop
|
|
507
|
+
const cond = term[1];
|
|
508
|
+
const targetLabel = extractLabel(term[2]);
|
|
509
|
+
if (targetLabel !== null) {
|
|
510
|
+
const targetState = labelToState.get(targetLabel);
|
|
511
|
+
if (targetState !== undefined) {
|
|
512
|
+
const skipLabel = compiler._makeLabel("cff_skip");
|
|
513
|
+
out.push([OP.JUMP_IF_TRUE, cond, {
|
|
514
|
+
type: "label",
|
|
515
|
+
label: skipLabel
|
|
516
|
+
}]);
|
|
517
|
+
emitStateTransition(out, rState, rDelta, block.stateValue, targetState, loopTopLabel, compiler);
|
|
518
|
+
out.push([null, {
|
|
519
|
+
type: "defineLabel",
|
|
520
|
+
label: skipLabel
|
|
521
|
+
}]);
|
|
522
|
+
emitStateTransition(out, rState, rDelta, block.stateValue, fallthroughStateMap.get(origIdx), loopTopLabel, compiler);
|
|
523
|
+
} else {
|
|
524
|
+
out.push(term);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
out.push(term);
|
|
528
|
+
}
|
|
529
|
+
} else if (term[0] === OP.JUMP_IF_TRUE) {
|
|
530
|
+
// Original: if (cond) goto target; else fallthrough
|
|
531
|
+
// → if (!cond) goto skipLabel (inverted)
|
|
532
|
+
// state = targetState; goto loopTop
|
|
533
|
+
// skipLabel:
|
|
534
|
+
// state = fallthroughState; goto loopTop
|
|
535
|
+
const cond = term[1];
|
|
536
|
+
const targetLabel = extractLabel(term[2]);
|
|
537
|
+
if (targetLabel !== null) {
|
|
538
|
+
const targetState = labelToState.get(targetLabel);
|
|
539
|
+
if (targetState !== undefined) {
|
|
540
|
+
const skipLabel = compiler._makeLabel("cff_skip");
|
|
541
|
+
out.push([OP.JUMP_IF_FALSE, cond, {
|
|
542
|
+
type: "label",
|
|
543
|
+
label: skipLabel
|
|
544
|
+
}]);
|
|
545
|
+
emitStateTransition(out, rState, rDelta, block.stateValue, targetState, loopTopLabel, compiler);
|
|
546
|
+
out.push([null, {
|
|
547
|
+
type: "defineLabel",
|
|
548
|
+
label: skipLabel
|
|
549
|
+
}]);
|
|
550
|
+
emitStateTransition(out, rState, rDelta, block.stateValue, fallthroughStateMap.get(origIdx), loopTopLabel, compiler);
|
|
551
|
+
} else {
|
|
552
|
+
out.push(term);
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
out.push(term);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
instrs: out,
|
|
561
|
+
tail: dispatch.innerBytecode
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Pass entry point ──────────────────────────────────────────────────────────
|
|
566
|
+
export function controlFlowFlattening(bc, compiler) {
|
|
567
|
+
const maxId = buildMaxIdMap(bc);
|
|
568
|
+
return forEachFunction(bc, compiler, (fnInstrs, fnId) => processFunctionBlock(fnInstrs, fnId, compiler, maxId));
|
|
569
|
+
}
|