js-confuser-vm 0.0.8 → 0.1.0
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/.gitmodules +4 -0
- package/CHANGELOG.md +102 -2
- package/README.md +95 -1
- package/dist/compiler.js +225 -152
- package/dist/runtime.js +200 -143
- package/dist/template.js +142 -0
- package/dist/transforms/bytecode/dispatcher.js +362 -0
- package/dist/transforms/bytecode/macroOpcodes.js +1 -1
- package/dist/transforms/bytecode/resolveLabels.js +21 -18
- package/dist/transforms/bytecode/resolveRegisters.js +212 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +4 -2
- package/dist/types.js +41 -0
- package/dist/utils/op-utils.js +1 -0
- package/index.ts +1 -0
- package/jest.config.js +5 -0
- package/package.json +10 -2
- package/src/compiler.ts +291 -180
- package/src/options.ts +1 -0
- package/src/runtime.ts +222 -141
- package/src/template.ts +141 -0
- package/src/transforms/bytecode/aliasedOpcodes.ts +1 -1
- package/src/transforms/bytecode/dispatcher.ts +398 -0
- package/src/transforms/bytecode/macroOpcodes.ts +2 -2
- package/src/transforms/bytecode/resolveLabels.ts +31 -27
- package/src/transforms/bytecode/resolveRegisters.ts +221 -0
- package/src/transforms/bytecode/specializedOpcodes.ts +5 -9
- package/src/types.ts +64 -4
- package/src/utils/op-utils.ts +2 -0
- package/dist/transforms/utils/op-utils.js +0 -25
- package/dist/transforms/utils/random-utils.js +0 -27
- package/dist/utilts.js +0 -3
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// resolveRegisters
|
|
2
|
+
// Converts virtual RegisterOperand objects into concrete slot indices and sets
|
|
3
|
+
// each FnDescriptor's regCount.
|
|
4
|
+
//
|
|
5
|
+
// Two-tier slot assignment:
|
|
6
|
+
//
|
|
7
|
+
// "local::" pool (params, `arguments`, hoisted vars, upvalue-captured vars)
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Sorted by virtual-id, slots assigned sequentially with NO reuse.
|
|
10
|
+
// This is required because:
|
|
11
|
+
// • The runtime writes args[i] to regs[base + i] at call time, so params
|
|
12
|
+
// MUST occupy slots 0..paramCount-1 in virtual-id order.
|
|
13
|
+
// • Open upvalues hold an absolute slot index and read regs[base+slot] for
|
|
14
|
+
// the lifetime of the outer frame — reusing a captured slot corrupts reads.
|
|
15
|
+
//
|
|
16
|
+
// All other pools (e.g. "temp::", "canary::", pass-introduced pools)
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Linear-scan with a free list: registers are sorted by firstUse, and any
|
|
19
|
+
// slot whose previous occupant's lastUse < current register's firstUse is
|
|
20
|
+
// recycled. An explicit [null, freeRegOperand(reg)] pseudo-instruction clamps
|
|
21
|
+
// lastUse early, enabling reuse before the natural end of the live range.
|
|
22
|
+
//
|
|
23
|
+
// Pools are processed in priority order: "local::" always first (slots
|
|
24
|
+
// 0..N), then remaining pools alphabetically. This keeps temp slots above
|
|
25
|
+
// the reserved param/local region.
|
|
26
|
+
//
|
|
27
|
+
// regCount = max concrete slot used across all pools + 1.
|
|
28
|
+
//
|
|
29
|
+
// Run AFTER all IR-level passes but BEFORE resolveLabels / resolveConstants.
|
|
30
|
+
|
|
31
|
+
export function resolveRegisters(bc, compiler) {
|
|
32
|
+
function registerPoolKey(op) {
|
|
33
|
+
return `${op.kind ?? "local"}::${op.scopeId ?? ""}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Pass 1: collect live ranges ───────────────────────────────────────────
|
|
37
|
+
// For each (fnId, virtId) record the first and last instruction index where
|
|
38
|
+
// the register appears as a real operand. A freeReg marker clamps lastUse.
|
|
39
|
+
|
|
40
|
+
// fnId -> virtId -> RegInfo
|
|
41
|
+
const fnRegInfo = new Map();
|
|
42
|
+
for (let i = 0; i < bc.length; i++) {
|
|
43
|
+
const instr = bc[i];
|
|
44
|
+
for (let j = 1; j < instr.length; j++) {
|
|
45
|
+
const op = instr[j];
|
|
46
|
+
if (!op || typeof op !== "object") continue;
|
|
47
|
+
if (op.type === "register") {
|
|
48
|
+
const {
|
|
49
|
+
fnId,
|
|
50
|
+
id
|
|
51
|
+
} = op;
|
|
52
|
+
const poolKey = registerPoolKey(op);
|
|
53
|
+
let fnMap = fnRegInfo.get(fnId);
|
|
54
|
+
if (!fnMap) {
|
|
55
|
+
fnMap = new Map();
|
|
56
|
+
fnRegInfo.set(fnId, fnMap);
|
|
57
|
+
}
|
|
58
|
+
const existing = fnMap.get(id);
|
|
59
|
+
if (!existing) {
|
|
60
|
+
fnMap.set(id, {
|
|
61
|
+
firstUse: i,
|
|
62
|
+
lastUse: i,
|
|
63
|
+
poolKey,
|
|
64
|
+
freed: false
|
|
65
|
+
});
|
|
66
|
+
} else if (!existing.freed) {
|
|
67
|
+
// Only extend lastUse if no explicit freeReg has clamped it yet.
|
|
68
|
+
existing.lastUse = i;
|
|
69
|
+
}
|
|
70
|
+
} else if (op.type === "freeReg") {
|
|
71
|
+
// Explicit end-of-life marker: clamp lastUse and prevent extension.
|
|
72
|
+
const {
|
|
73
|
+
fnId,
|
|
74
|
+
id
|
|
75
|
+
} = op;
|
|
76
|
+
const fnMap = fnRegInfo.get(fnId);
|
|
77
|
+
if (fnMap) {
|
|
78
|
+
const info = fnMap.get(id);
|
|
79
|
+
if (info && !info.freed) {
|
|
80
|
+
info.lastUse = i;
|
|
81
|
+
info.freed = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Pass 2: slot assignment per function ──────────────────────────────────
|
|
89
|
+
// fnId -> virtId -> concrete slot
|
|
90
|
+
const fnSlotMaps = new Map();
|
|
91
|
+
|
|
92
|
+
// Pool ordering: "local::" always first; all other keys sorted alphabetically.
|
|
93
|
+
function poolSortKey(key) {
|
|
94
|
+
return key === "local::" ? [0, ""] : [1, key];
|
|
95
|
+
}
|
|
96
|
+
for (const [fnId, regMap] of fnRegInfo) {
|
|
97
|
+
// Group by pool key.
|
|
98
|
+
const pools = new Map();
|
|
99
|
+
for (const [id, info] of regMap) {
|
|
100
|
+
let pool = pools.get(info.poolKey);
|
|
101
|
+
if (!pool) {
|
|
102
|
+
pool = [];
|
|
103
|
+
pools.set(info.poolKey, pool);
|
|
104
|
+
}
|
|
105
|
+
pool.push({
|
|
106
|
+
id,
|
|
107
|
+
firstUse: info.firstUse,
|
|
108
|
+
lastUse: info.lastUse
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const sortedPoolKeys = Array.from(pools.keys()).sort((a, b) => {
|
|
112
|
+
const [pa, sa] = poolSortKey(a);
|
|
113
|
+
const [pb, sb] = poolSortKey(b);
|
|
114
|
+
if (pa !== pb) return pa - pb;
|
|
115
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
116
|
+
});
|
|
117
|
+
const slotMap = new Map(); // virtId -> slot
|
|
118
|
+
fnSlotMaps.set(fnId, slotMap);
|
|
119
|
+
|
|
120
|
+
// nextSlot is the high-water mark: the next fresh slot to allocate.
|
|
121
|
+
// It is shared across all pools so each pool's slots start above the
|
|
122
|
+
// previous pool's maximum slot.
|
|
123
|
+
let nextSlot = 0;
|
|
124
|
+
for (const poolKey of sortedPoolKeys) {
|
|
125
|
+
const regs = pools.get(poolKey);
|
|
126
|
+
if (poolKey === "local::") {
|
|
127
|
+
// ── Local pool: virtual-id order, no reuse ────────────────────────
|
|
128
|
+
// Params must be at the lowest slots (written by the runtime at call
|
|
129
|
+
// time); upvalue captures must keep their slot for the frame's lifetime.
|
|
130
|
+
regs.sort((a, b) => a.id - b.id);
|
|
131
|
+
for (const reg of regs) {
|
|
132
|
+
slotMap.set(reg.id, nextSlot++);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// ── Non-local pool: firstUse order, linear-scan reuse ─────────────
|
|
136
|
+
regs.sort((a, b) => a.firstUse - b.firstUse);
|
|
137
|
+
|
|
138
|
+
// freeList entries: { slot, freeAt } where freeAt = lastUse of current
|
|
139
|
+
// occupant. A slot becomes available when freeAt < next reg's firstUse.
|
|
140
|
+
const freeList = [];
|
|
141
|
+
for (const reg of regs) {
|
|
142
|
+
// Find the lowest-numbered slot whose last occupant has ended.
|
|
143
|
+
let bestSlot = -1;
|
|
144
|
+
let bestIdx = -1;
|
|
145
|
+
for (let k = 0; k < freeList.length; k++) {
|
|
146
|
+
if (freeList[k].freeAt < reg.firstUse) {
|
|
147
|
+
if (bestSlot === -1 || freeList[k].slot < bestSlot) {
|
|
148
|
+
bestSlot = freeList[k].slot;
|
|
149
|
+
bestIdx = k;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let assignedSlot;
|
|
154
|
+
if (bestIdx !== -1) {
|
|
155
|
+
assignedSlot = bestSlot;
|
|
156
|
+
freeList.splice(bestIdx, 1);
|
|
157
|
+
} else {
|
|
158
|
+
assignedSlot = nextSlot++;
|
|
159
|
+
}
|
|
160
|
+
slotMap.set(reg.id, assignedSlot);
|
|
161
|
+
freeList.push({
|
|
162
|
+
slot: assignedSlot,
|
|
163
|
+
freeAt: reg.lastUse
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// nextSlot already reflects the high-water mark; reused slots are
|
|
167
|
+
// always < nextSlot by construction.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Pass 3: patch register operands ──────────────────────────────────────
|
|
173
|
+
for (const instr of bc) {
|
|
174
|
+
for (let i = 1; i < instr.length; i++) {
|
|
175
|
+
const op = instr[i];
|
|
176
|
+
if (!op || typeof op !== "object") continue;
|
|
177
|
+
if (op.type === "register") {
|
|
178
|
+
op.resolvedValue = fnSlotMaps.get(op.fnId)?.get(op.id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Pass 4: set regCount on each FnDescriptor ─────────────────────────────
|
|
184
|
+
// regCount = max concrete slot used + 1 (not sum of virtual-register counts).
|
|
185
|
+
for (const desc of compiler.fnDescriptors) {
|
|
186
|
+
const fnId = desc._fnIdx;
|
|
187
|
+
const slotMap = fnSlotMaps.get(fnId);
|
|
188
|
+
let regCount = 0;
|
|
189
|
+
if (slotMap) {
|
|
190
|
+
for (const slot of slotMap.values()) {
|
|
191
|
+
if (slot + 1 > regCount) regCount = slot + 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
desc.regCount = regCount;
|
|
195
|
+
}
|
|
196
|
+
compiler.mainRegCount = compiler.mainFn?.regCount ?? 0;
|
|
197
|
+
|
|
198
|
+
// ── Pass 5: patch fnRegCount operands ────────────────────────────────────
|
|
199
|
+
for (const instr of bc) {
|
|
200
|
+
for (let i = 1; i < instr.length; i++) {
|
|
201
|
+
const op = instr[i];
|
|
202
|
+
if (!op || typeof op !== "object") continue;
|
|
203
|
+
if (op.type === "fnRegCount") {
|
|
204
|
+
const desc = compiler.fnDescriptors[op.fnId];
|
|
205
|
+
op.resolvedValue = desc?.regCount ?? 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
bytecode: bc
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -4,9 +4,11 @@ export const nSizedOps = ["MAKE_CLOSURE", "BUILD_ARRAY", "BUILD_OBJECT", "CALL",
|
|
|
4
4
|
|
|
5
5
|
// Creates specialized opcodes for the most frequent (OPCODE + single_integer_operand) pairs.
|
|
6
6
|
// Example: [OP.LOAD_CONST, 1] becomes [SPECIALIZED_LOAD_CONST_1].
|
|
7
|
-
// Only instructions
|
|
7
|
+
// Only instructions that are fixed-sized are considered.
|
|
8
8
|
// MAKE_CLOSURE and other N-sized instructions cannot be specialized
|
|
9
|
-
//
|
|
9
|
+
// Operands are converted into objects and marked as 'placeholder' - other passes can mutate and the reference stays intact
|
|
10
|
+
// We need a reference throughout the pipeline so that final AST generation can place the actual value
|
|
11
|
+
// The 'placeholder' flag drops the operand from the final bytecode - any size calculation must not count these
|
|
10
12
|
export function specializedOpcodes(bc, compiler) {
|
|
11
13
|
const disallowedOps = new Set(nSizedOps.map(name => compiler.OP[name]));
|
|
12
14
|
|
package/dist/types.js
CHANGED
|
@@ -6,10 +6,51 @@
|
|
|
6
6
|
// All "null" instructions are dropped before assembly time.
|
|
7
7
|
// Instructions may carry any number of operands; the flat output serializes
|
|
8
8
|
// each operand as a separate u16 slot in the bytecode array.
|
|
9
|
+
// A virtual register reference emitted by the compiler.
|
|
10
|
+
// fnId identifies which function's register file this belongs to.
|
|
11
|
+
// resolveRegisters() replaces these with concrete slot indices (type:"number").
|
|
12
|
+
|
|
13
|
+
// A placeholder for a function's concrete regCount, emitted in MAKE_CLOSURE.
|
|
14
|
+
// resolveRegisters() fills resolvedValue once it knows the concrete slot count.
|
|
15
|
+
|
|
16
|
+
// IR pseudo-instruction that marks the end of a register's live range.
|
|
17
|
+
// Emitted as [null, FreeRegOperand] so it is dropped before final assembly.
|
|
18
|
+
//
|
|
19
|
+
// NOTE: resolveRegisters() already computes correct lastUse from the last real
|
|
20
|
+
// operand appearance, so freeReg is EXTRANEOUS for any programmatically generated
|
|
21
|
+
// IR — the scanner will find the tightest possible range without it.
|
|
22
|
+
// It is only useful when a register has a late syntactic appearance that does
|
|
23
|
+
// NOT reflect its true logical end-of-life (e.g. a read emitted purely for
|
|
24
|
+
// side-effects long after the value is logically dead). No current pass in this
|
|
25
|
+
// codebase emits freeReg; it is kept as an extension point only.
|
|
9
26
|
|
|
10
27
|
export function constantOperand(value) {
|
|
11
28
|
return {
|
|
12
29
|
type: "constant",
|
|
13
30
|
value: value
|
|
14
31
|
};
|
|
32
|
+
}
|
|
33
|
+
export function registerOperand(id, fnId, metadata = {}) {
|
|
34
|
+
return {
|
|
35
|
+
type: "register",
|
|
36
|
+
id,
|
|
37
|
+
fnId,
|
|
38
|
+
...metadata
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function fnRegCountOperand(fnId) {
|
|
42
|
+
return {
|
|
43
|
+
type: "fnRegCount",
|
|
44
|
+
fnId
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function freeRegOperand(reg) {
|
|
48
|
+
const op = {
|
|
49
|
+
type: "freeReg",
|
|
50
|
+
fnId: reg.fnId,
|
|
51
|
+
id: reg.id
|
|
52
|
+
};
|
|
53
|
+
if (reg.kind !== undefined) op.kind = reg.kind;
|
|
54
|
+
if (reg.scopeId !== undefined) op.scopeId = reg.scopeId;
|
|
55
|
+
return op;
|
|
15
56
|
}
|
package/dist/utils/op-utils.js
CHANGED
package/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ async function main() {
|
|
|
13
13
|
shuffleOpcodes: true, // shuffle order of opcode handlers in the runtime?
|
|
14
14
|
encodeBytecode: true, // encode the bytecode array?
|
|
15
15
|
concealConstants: true, // conceal strings and integers in the constant pool?
|
|
16
|
+
dispatcher: true, // create middleman blocks to process jumps?
|
|
16
17
|
selfModifying: true, // do self-modifying bytecode for function bodies?
|
|
17
18
|
macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
|
|
18
19
|
microOpcodes: true, // break opcodes into sub-opcodes?
|
package/jest.config.js
CHANGED
|
@@ -19,6 +19,10 @@ const OPTIONS_MATRIX = [
|
|
|
19
19
|
displayName: "concealConstants",
|
|
20
20
|
VM_OPTIONS: { concealConstants: true },
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
displayName: "dispatcher",
|
|
24
|
+
VM_OPTIONS: { dispatcher: true },
|
|
25
|
+
},
|
|
22
26
|
{
|
|
23
27
|
displayName: "all",
|
|
24
28
|
VM_OPTIONS: {
|
|
@@ -32,6 +36,7 @@ const OPTIONS_MATRIX = [
|
|
|
32
36
|
specializedOpcodes: true,
|
|
33
37
|
aliasedOpcodes: true,
|
|
34
38
|
concealConstants: true,
|
|
39
|
+
dispatcher: true,
|
|
35
40
|
},
|
|
36
41
|
},
|
|
37
42
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "js-confuser-vm",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"author": "MichaelXF",
|
|
25
25
|
"license": "MIT",
|
|
26
|
-
"description": "",
|
|
26
|
+
"description": "Experimental JS VM Obfuscator",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@babel/generator": "^7.29.1",
|
|
29
29
|
"@babel/parser": "^7.29.0",
|
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
"glob": "^13.0.6",
|
|
44
44
|
"jest": "^30.2.0"
|
|
45
45
|
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/MichaelXF/js-confuser-vm"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/MichaelXF/js-confuser-vm/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://js-confuser.com/vm",
|
|
46
54
|
"engines": {
|
|
47
55
|
"node": ">=18.0.0"
|
|
48
56
|
}
|