exoproc-accessors 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/LICENSE +235 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1660 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware-accessor.d.ts +346 -0
- package/dist/middleware-accessor.d.ts.map +1 -0
- package/dist/middleware-accessor.js +1870 -0
- package/dist/middleware-accessor.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,1870 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { NativeMemory, normalizeType, resolveAddress, alignUp, computeNearAllocRange, freeRegionCandidate, AllocNearRangeError, Kernel32Impl, MsvcrtImpl, MsvcrtLibrary, MemoryBasicInformation, PROCESSENTRY32W_SIZE, MemoryState, MemoryProtection, MemoryFreeType, ToolhelpSnapshotFlag, ProcessAccess, DEFAULT_MACHINECODE_ALIGNMENT, memmem, memmem1, memmem2, memmem4, memmem8, memmemWithoutBuffer, Pattern, Scanner, verifyCoreModules, verifyCoreModulesSync, isModuleLoadedInProcess, isModuleLoadedInProcessSync, MiddlewareAccessor, InittableMiddlewareAccessor, isMiddlewareAccessor, } from 'bun-xffi';
|
|
6
|
+
export class MsvcrtDependentMiddlewareAccessor extends InittableMiddlewareAccessor {
|
|
7
|
+
async onInit() {
|
|
8
|
+
if (this.isLocal)
|
|
9
|
+
return;
|
|
10
|
+
const msvcrtBase = MsvcrtLibrary.baseAddress;
|
|
11
|
+
const isMsvcrtLoaded = await isModuleLoadedInProcess(this.root, msvcrtBase);
|
|
12
|
+
if (!isMsvcrtLoaded) {
|
|
13
|
+
throw new Error('Target process does not have msvcrt.dll loaded.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
onInitSync() {
|
|
17
|
+
if (this.isLocal)
|
|
18
|
+
return;
|
|
19
|
+
const msvcrtBase = MsvcrtLibrary.baseAddress;
|
|
20
|
+
const isMsvcrtLoaded = isModuleLoadedInProcessSync(this.root, msvcrtBase);
|
|
21
|
+
if (!isMsvcrtLoaded) {
|
|
22
|
+
throw new Error('Target process does not have msvcrt.dll loaded.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* HostAccessor is a base class that automatically initializes all nested InittableMiddlewareAccessors
|
|
28
|
+
* in the backend decorator chain.
|
|
29
|
+
*/
|
|
30
|
+
export class HostAccessor extends InittableMiddlewareAccessor {
|
|
31
|
+
get processId() {
|
|
32
|
+
return this._processId;
|
|
33
|
+
}
|
|
34
|
+
constructor(backend, root) {
|
|
35
|
+
super(backend, root ?? null);
|
|
36
|
+
if (!root) {
|
|
37
|
+
this.root = this;
|
|
38
|
+
}
|
|
39
|
+
let b = backend;
|
|
40
|
+
while (isMiddlewareAccessor(b)) {
|
|
41
|
+
b = b.backend;
|
|
42
|
+
}
|
|
43
|
+
if (b) {
|
|
44
|
+
this._processId = b.processId;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async onInit() {
|
|
48
|
+
// No-op. Chain initialization is automatically propagated by init().
|
|
49
|
+
}
|
|
50
|
+
onInitSync() {
|
|
51
|
+
// No-op. Chain initialization is automatically propagated by initSync().
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A HostAccessor that forwards all operations to a dynamically changeable target HostAccessor.
|
|
56
|
+
* This is useful for resolving circular dependencies in middleware chains where the root
|
|
57
|
+
* reference is readonly and needs to be set after construction.
|
|
58
|
+
*
|
|
59
|
+
* Usage:
|
|
60
|
+
* const redirector = new RedirectorHostAccessor(pid);
|
|
61
|
+
* // ... build middleware chain using redirector as root ...
|
|
62
|
+
* redirector.target = actualHostAccessor; // wire it up later
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* HostAccessor that throws an error for all operations. Used as the default/unconfigured target for redirectors.
|
|
66
|
+
*/
|
|
67
|
+
export class ThrowingHostAccessor extends HostAccessor {
|
|
68
|
+
constructor(processId, root) {
|
|
69
|
+
super(new ThrowingMemoryAccessor(processId), root);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A HostAccessor that forwards all operations to a dynamically changeable target HostAccessor.
|
|
74
|
+
* If target is set to `this`, it delegates calls directly to its own backend (acting like a normal HostAccessor).
|
|
75
|
+
*/
|
|
76
|
+
export class RedirectorHostAccessor extends HostAccessor {
|
|
77
|
+
_target;
|
|
78
|
+
constructor(processId, root) {
|
|
79
|
+
super(new ThrowingMemoryAccessor(processId), root);
|
|
80
|
+
this._target = new ThrowingHostAccessor(processId, this.root);
|
|
81
|
+
}
|
|
82
|
+
get target() {
|
|
83
|
+
return this._target;
|
|
84
|
+
}
|
|
85
|
+
set target(value) {
|
|
86
|
+
this._target = value;
|
|
87
|
+
this._processId = value.processId;
|
|
88
|
+
}
|
|
89
|
+
getTarget() {
|
|
90
|
+
return this._target;
|
|
91
|
+
}
|
|
92
|
+
async read(address, size, offset = 0) {
|
|
93
|
+
const t = this.getTarget();
|
|
94
|
+
if (t === this) {
|
|
95
|
+
return super.read(address, size, offset);
|
|
96
|
+
}
|
|
97
|
+
return t.read(address, size, offset);
|
|
98
|
+
}
|
|
99
|
+
async write(address, data, offset = 0) {
|
|
100
|
+
const t = this.getTarget();
|
|
101
|
+
if (t === this) {
|
|
102
|
+
return super.write(address, data, offset);
|
|
103
|
+
}
|
|
104
|
+
return t.write(address, data, offset);
|
|
105
|
+
}
|
|
106
|
+
async alloc(size, address = null, protection, allocationType) {
|
|
107
|
+
const t = this.getTarget();
|
|
108
|
+
if (t === this) {
|
|
109
|
+
return super.alloc(size, address, protection, allocationType);
|
|
110
|
+
}
|
|
111
|
+
return t.alloc(size, address, protection, allocationType);
|
|
112
|
+
}
|
|
113
|
+
async free(address, size = 0, freeType) {
|
|
114
|
+
const t = this.getTarget();
|
|
115
|
+
if (t === this) {
|
|
116
|
+
return super.free(address, size, freeType);
|
|
117
|
+
}
|
|
118
|
+
return t.free(address, size, freeType);
|
|
119
|
+
}
|
|
120
|
+
async protect(address, size, newProtect) {
|
|
121
|
+
const t = this.getTarget();
|
|
122
|
+
if (t === this) {
|
|
123
|
+
return super.protect(address, size, newProtect);
|
|
124
|
+
}
|
|
125
|
+
return t.protect(address, size, newProtect);
|
|
126
|
+
}
|
|
127
|
+
async query(address) {
|
|
128
|
+
const t = this.getTarget();
|
|
129
|
+
if (t === this) {
|
|
130
|
+
return super.query(address);
|
|
131
|
+
}
|
|
132
|
+
return t.query(address);
|
|
133
|
+
}
|
|
134
|
+
async call(func, ...args) {
|
|
135
|
+
const t = this.getTarget();
|
|
136
|
+
if (t === this) {
|
|
137
|
+
return super.call(func, ...args);
|
|
138
|
+
}
|
|
139
|
+
return t.call(func, ...args);
|
|
140
|
+
}
|
|
141
|
+
async machineCode(machineCode) {
|
|
142
|
+
const t = this.getTarget();
|
|
143
|
+
if (t === this) {
|
|
144
|
+
return super.machineCode(machineCode);
|
|
145
|
+
}
|
|
146
|
+
return t.machineCode(machineCode);
|
|
147
|
+
}
|
|
148
|
+
async *scan(address, size, pattern) {
|
|
149
|
+
const t = this.getTarget();
|
|
150
|
+
if (t === this) {
|
|
151
|
+
yield* super.scan(address, size, pattern);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
yield* t.scan(address, size, pattern);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ── Sync twins of the overrides above -- same getTarget()-redirect, for
|
|
158
|
+
// callers (e.g. NThread.callSync) that need the *Sync family routed
|
|
159
|
+
// through `root` all the way to whatever `target` ends up wired to,
|
|
160
|
+
// instead of silently hitting the placeholder ThrowingMemoryAccessor
|
|
161
|
+
// this class starts with as its own `backend`. ─────────────────────────
|
|
162
|
+
readSync(address, size, offset = 0) {
|
|
163
|
+
const t = this.getTarget();
|
|
164
|
+
if (t === this) {
|
|
165
|
+
return super.readSync(address, size, offset);
|
|
166
|
+
}
|
|
167
|
+
return t.readSync(address, size, offset);
|
|
168
|
+
}
|
|
169
|
+
writeSync(address, data, offset = 0) {
|
|
170
|
+
const t = this.getTarget();
|
|
171
|
+
if (t === this) {
|
|
172
|
+
return super.writeSync(address, data, offset);
|
|
173
|
+
}
|
|
174
|
+
return t.writeSync(address, data, offset);
|
|
175
|
+
}
|
|
176
|
+
allocSync(size, address = null, protection, allocationType) {
|
|
177
|
+
const t = this.getTarget();
|
|
178
|
+
if (t === this) {
|
|
179
|
+
return super.allocSync(size, address, protection, allocationType);
|
|
180
|
+
}
|
|
181
|
+
return t.allocSync(size, address, protection, allocationType);
|
|
182
|
+
}
|
|
183
|
+
freeSync(address, size = 0, freeType) {
|
|
184
|
+
const t = this.getTarget();
|
|
185
|
+
if (t === this) {
|
|
186
|
+
return super.freeSync(address, size, freeType);
|
|
187
|
+
}
|
|
188
|
+
return t.freeSync(address, size, freeType);
|
|
189
|
+
}
|
|
190
|
+
protectSync(address, size, newProtect) {
|
|
191
|
+
const t = this.getTarget();
|
|
192
|
+
if (t === this) {
|
|
193
|
+
return super.protectSync(address, size, newProtect);
|
|
194
|
+
}
|
|
195
|
+
return t.protectSync(address, size, newProtect);
|
|
196
|
+
}
|
|
197
|
+
querySync(address) {
|
|
198
|
+
const t = this.getTarget();
|
|
199
|
+
if (t === this) {
|
|
200
|
+
return super.querySync(address);
|
|
201
|
+
}
|
|
202
|
+
return t.querySync(address);
|
|
203
|
+
}
|
|
204
|
+
callSync(func, ...args) {
|
|
205
|
+
const t = this.getTarget();
|
|
206
|
+
if (t === this) {
|
|
207
|
+
return super.callSync(func, ...args);
|
|
208
|
+
}
|
|
209
|
+
return t.callSync(func, ...args);
|
|
210
|
+
}
|
|
211
|
+
machineCodeSync(machineCode) {
|
|
212
|
+
const t = this.getTarget();
|
|
213
|
+
if (t === this) {
|
|
214
|
+
return super.machineCodeSync(machineCode);
|
|
215
|
+
}
|
|
216
|
+
return t.machineCodeSync(machineCode);
|
|
217
|
+
}
|
|
218
|
+
*scanSync(address, size, pattern) {
|
|
219
|
+
const t = this.getTarget();
|
|
220
|
+
if (t === this) {
|
|
221
|
+
yield* super.scanSync(address, size, pattern);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
yield* t.scanSync(address, size, pattern);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
close() {
|
|
228
|
+
if (this._target && this._target !== this) {
|
|
229
|
+
this._target.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async onInit() {
|
|
233
|
+
// No-op. The target is initialized separately.
|
|
234
|
+
}
|
|
235
|
+
onInitSync() {
|
|
236
|
+
// No-op. The target is initialized separately.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* BootstrapHostAccessor is a specialized RedirectorHostAccessor designed to resolve circular dependencies
|
|
241
|
+
* during the initialization of decorator/middleware chains.
|
|
242
|
+
* It acts as a temporary root, redirecting calls to itself (direct backend execution)
|
|
243
|
+
* during initialization, and then automatically switches to the actual root once its backend is initialized.
|
|
244
|
+
*/
|
|
245
|
+
export class BootstrapHostAccessor extends RedirectorHostAccessor {
|
|
246
|
+
constructor(processId, root) {
|
|
247
|
+
super(processId, root);
|
|
248
|
+
this.target = this;
|
|
249
|
+
}
|
|
250
|
+
async onInit() {
|
|
251
|
+
await this.initNext();
|
|
252
|
+
this.target = this.root;
|
|
253
|
+
}
|
|
254
|
+
onInitSync() {
|
|
255
|
+
this.initNextSync();
|
|
256
|
+
this.target = this.root;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Advanced Middleware Accessor that redirects virtual memory operations
|
|
261
|
+
* (alloc, free, protect, query) to run inside the target process's context via the call method.
|
|
262
|
+
* This performs memory operations directly in the target process's thread context, optimizing access and avoiding cross-process overhead.
|
|
263
|
+
*/
|
|
264
|
+
export class CallRedirectorAccessor extends MiddlewareAccessor {
|
|
265
|
+
async alloc(size, address = null, protection = MemoryProtection.READWRITE, allocationType = MemoryState.COMMIT | MemoryState.RESERVE) {
|
|
266
|
+
const addressVal = address ? resolveAddress(address) : null;
|
|
267
|
+
const result = await this.root.call(Kernel32Impl.VirtualAlloc, addressVal, size, allocationType, protection);
|
|
268
|
+
if (!result || Number(result) === 0) {
|
|
269
|
+
throw new Error(`VirtualAlloc failed in remote process for size ${size}`);
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
allocSync(size, address = null, protection = MemoryProtection.READWRITE, allocationType = MemoryState.COMMIT | MemoryState.RESERVE) {
|
|
274
|
+
const addressVal = address ? resolveAddress(address) : null;
|
|
275
|
+
const result = this.root.callSync(Kernel32Impl.VirtualAlloc, addressVal, size, allocationType, protection);
|
|
276
|
+
if (!result || Number(result) === 0) {
|
|
277
|
+
throw new Error(`VirtualAlloc failed in remote process for size ${size}`);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
async free(address, size = 0, freeType = MemoryFreeType.RELEASE) {
|
|
282
|
+
const success = await this.root.call(Kernel32Impl.VirtualFree, resolveAddress(address), size, freeType);
|
|
283
|
+
return success !== 0 && success !== false;
|
|
284
|
+
}
|
|
285
|
+
freeSync(address, size = 0, freeType = MemoryFreeType.RELEASE) {
|
|
286
|
+
const success = this.root.callSync(Kernel32Impl.VirtualFree, resolveAddress(address), size, freeType);
|
|
287
|
+
return success !== 0 && success !== false;
|
|
288
|
+
}
|
|
289
|
+
async protect(address, size, newProtect) {
|
|
290
|
+
// Allocate a temporary 4-byte buffer in the target process using root.alloc
|
|
291
|
+
// to store the old protection value returned by VirtualProtect
|
|
292
|
+
const tempAddr = await this.root.alloc(4);
|
|
293
|
+
try {
|
|
294
|
+
const success = await this.root.call(Kernel32Impl.VirtualProtect, resolveAddress(address), size, newProtect, tempAddr);
|
|
295
|
+
if (!success || success === 0) {
|
|
296
|
+
throw new Error(`VirtualProtect failed in remote process at address ${resolveAddress(address)}`);
|
|
297
|
+
}
|
|
298
|
+
const oldProtect = await this.root.readUInt32(tempAddr);
|
|
299
|
+
return oldProtect;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
await this.root.free(tempAddr);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
protectSync(address, size, newProtect) {
|
|
306
|
+
// Sync twin of protect() -- see its comment for the temp-buffer rationale.
|
|
307
|
+
const tempAddr = this.root.allocSync(4);
|
|
308
|
+
try {
|
|
309
|
+
const success = this.root.callSync(Kernel32Impl.VirtualProtect, resolveAddress(address), size, newProtect, tempAddr);
|
|
310
|
+
if (!success || success === 0) {
|
|
311
|
+
throw new Error(`VirtualProtect failed in remote process at address ${resolveAddress(address)}`);
|
|
312
|
+
}
|
|
313
|
+
return this.root.readUInt32Sync(tempAddr);
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
this.root.freeSync(tempAddr);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async query(address) {
|
|
320
|
+
const info = new MemoryBasicInformation();
|
|
321
|
+
// Allocate a temporary 48-byte buffer in the target process using root.alloc
|
|
322
|
+
// to store the MEMORY_BASIC_INFORMATION structure
|
|
323
|
+
const tempAddr = await this.root.alloc(info.size);
|
|
324
|
+
try {
|
|
325
|
+
const bytesReturned = await this.root.call(Kernel32Impl.VirtualQuery, resolveAddress(address), tempAddr, info.size);
|
|
326
|
+
if (!bytesReturned || Number(bytesReturned) === 0) {
|
|
327
|
+
throw new Error(`VirtualQuery failed in remote process at address ${resolveAddress(address)}`);
|
|
328
|
+
}
|
|
329
|
+
const buffer = await this.root.read(tempAddr, info.size);
|
|
330
|
+
info.writeSync(buffer, 0);
|
|
331
|
+
return info;
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
await this.root.free(tempAddr);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
querySync(address) {
|
|
338
|
+
// Sync twin of query() -- see its comment for the temp-buffer rationale.
|
|
339
|
+
const info = new MemoryBasicInformation();
|
|
340
|
+
const tempAddr = this.root.allocSync(info.size);
|
|
341
|
+
try {
|
|
342
|
+
const bytesReturned = this.root.callSync(Kernel32Impl.VirtualQuery, resolveAddress(address), tempAddr, info.size);
|
|
343
|
+
if (!bytesReturned || Number(bytesReturned) === 0) {
|
|
344
|
+
throw new Error(`VirtualQuery failed in remote process at address ${resolveAddress(address)}`);
|
|
345
|
+
}
|
|
346
|
+
const buffer = this.root.readSync(tempAddr, info.size);
|
|
347
|
+
info.writeSync(buffer, 0);
|
|
348
|
+
return info;
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
this.root.freeSync(tempAddr);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Same principle as this class's alloc/free/protect/query: the *target*
|
|
356
|
+
* process does the work itself via `call` (in-process VirtualQuery /
|
|
357
|
+
* VirtualAlloc), not VirtualQueryEx/VirtualAllocEx reaching in from ours.
|
|
358
|
+
*
|
|
359
|
+
* `allocNear` is a search loop rather than a single Win32 call, so this
|
|
360
|
+
* override drives it entirely through the sibling `this.query()` /
|
|
361
|
+
* `this.alloc()` (each already a remote `call`, and each still composed with
|
|
362
|
+
* subclasses like {@link IndirectCallRedirectorAccessor}). Because every probe
|
|
363
|
+
* is an expensive round-trip here, it walks one region per `VirtualQuery`
|
|
364
|
+
* using the reported `RegionSize`, instead of the base accessor's naive
|
|
365
|
+
* fixed 64KB grid steps.
|
|
366
|
+
*/
|
|
367
|
+
async allocNear(target, size, options = {}) {
|
|
368
|
+
const range = computeNearAllocRange(target, options);
|
|
369
|
+
const protection = options.protection ?? MemoryProtection.EXECUTE_READWRITE;
|
|
370
|
+
const allocationType = (MemoryState.COMMIT |
|
|
371
|
+
MemoryState.RESERVE);
|
|
372
|
+
// Try to claim a free region: VirtualAlloc runs in the target (sibling
|
|
373
|
+
// override) at the exact granularity-aligned candidate -- the allocation
|
|
374
|
+
// is made by the process itself.
|
|
375
|
+
const tryRegion = async (info) => {
|
|
376
|
+
const candidate = freeRegionCandidate(info, size, range);
|
|
377
|
+
if (candidate === null)
|
|
378
|
+
return null;
|
|
379
|
+
try {
|
|
380
|
+
return await this.alloc(size, candidate, protection, allocationType);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Region got claimed/rejected between query and alloc -- skip it.
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
// Probe the target's own region once, then walk outward from its edges.
|
|
388
|
+
const first = await this.query(range.target);
|
|
389
|
+
const firstHit = await tryRegion(first);
|
|
390
|
+
if (firstHit !== null)
|
|
391
|
+
return firstHit;
|
|
392
|
+
const firstBase = BigInt(resolveAddress(first.BaseAddress));
|
|
393
|
+
const firstSize = BigInt(first.RegionSize) || 0x10000n;
|
|
394
|
+
// Downward: each VirtualQuery jumps to the region just below the last.
|
|
395
|
+
let cursor = firstBase - 1n;
|
|
396
|
+
while (cursor >= range.minAddr) {
|
|
397
|
+
const info = await this.query(cursor);
|
|
398
|
+
const hit = await tryRegion(info);
|
|
399
|
+
if (hit !== null)
|
|
400
|
+
return hit;
|
|
401
|
+
const base = BigInt(resolveAddress(info.BaseAddress));
|
|
402
|
+
const next = base - 1n;
|
|
403
|
+
if (next >= cursor)
|
|
404
|
+
break; // no downward progress -- stop
|
|
405
|
+
cursor = next;
|
|
406
|
+
}
|
|
407
|
+
// Upward: each VirtualQuery jumps to the first address past the last region.
|
|
408
|
+
cursor = firstBase + firstSize;
|
|
409
|
+
while (cursor <= range.maxAddr) {
|
|
410
|
+
const info = await this.query(cursor);
|
|
411
|
+
const hit = await tryRegion(info);
|
|
412
|
+
if (hit !== null)
|
|
413
|
+
return hit;
|
|
414
|
+
const base = BigInt(resolveAddress(info.BaseAddress));
|
|
415
|
+
const regionSize = BigInt(info.RegionSize) || 0x10000n;
|
|
416
|
+
const next = base + regionSize;
|
|
417
|
+
if (next <= cursor)
|
|
418
|
+
break; // no upward progress -- stop
|
|
419
|
+
cursor = next;
|
|
420
|
+
}
|
|
421
|
+
throw new AllocNearRangeError(range.target, range.maxDistance);
|
|
422
|
+
}
|
|
423
|
+
allocNearSync(target, size, options = {}) {
|
|
424
|
+
// Sync twin of allocNear() -- same walk, same shared computeNearAllocRange/
|
|
425
|
+
// freeRegionCandidate helpers, driven with querySync/allocSync instead.
|
|
426
|
+
const range = computeNearAllocRange(target, options);
|
|
427
|
+
const protection = options.protection ?? MemoryProtection.EXECUTE_READWRITE;
|
|
428
|
+
const allocationType = (MemoryState.COMMIT |
|
|
429
|
+
MemoryState.RESERVE);
|
|
430
|
+
const tryRegion = (info) => {
|
|
431
|
+
const candidate = freeRegionCandidate(info, size, range);
|
|
432
|
+
if (candidate === null)
|
|
433
|
+
return null;
|
|
434
|
+
try {
|
|
435
|
+
return this.allocSync(size, candidate, protection, allocationType);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const first = this.querySync(range.target);
|
|
442
|
+
const firstHit = tryRegion(first);
|
|
443
|
+
if (firstHit !== null)
|
|
444
|
+
return firstHit;
|
|
445
|
+
const firstBase = BigInt(resolveAddress(first.BaseAddress));
|
|
446
|
+
const firstSize = BigInt(first.RegionSize) || 0x10000n;
|
|
447
|
+
let cursor = firstBase - 1n;
|
|
448
|
+
while (cursor >= range.minAddr) {
|
|
449
|
+
const info = this.querySync(cursor);
|
|
450
|
+
const hit = tryRegion(info);
|
|
451
|
+
if (hit !== null)
|
|
452
|
+
return hit;
|
|
453
|
+
const base = BigInt(resolveAddress(info.BaseAddress));
|
|
454
|
+
const next = base - 1n;
|
|
455
|
+
if (next >= cursor)
|
|
456
|
+
break;
|
|
457
|
+
cursor = next;
|
|
458
|
+
}
|
|
459
|
+
cursor = firstBase + firstSize;
|
|
460
|
+
while (cursor <= range.maxAddr) {
|
|
461
|
+
const info = this.querySync(cursor);
|
|
462
|
+
const hit = tryRegion(info);
|
|
463
|
+
if (hit !== null)
|
|
464
|
+
return hit;
|
|
465
|
+
const base = BigInt(resolveAddress(info.BaseAddress));
|
|
466
|
+
const regionSize = BigInt(info.RegionSize) || 0x10000n;
|
|
467
|
+
const next = base + regionSize;
|
|
468
|
+
if (next <= cursor)
|
|
469
|
+
break;
|
|
470
|
+
cursor = next;
|
|
471
|
+
}
|
|
472
|
+
throw new AllocNearRangeError(range.target, range.maxDistance);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Indirect variant of CallRedirectorAccessor that redirects read-write memory allocations
|
|
477
|
+
* to malloc on the process's CRT heap instead of VirtualAlloc.
|
|
478
|
+
* It tracks these heap allocations locally and intercepts free, protect, and query operations
|
|
479
|
+
* on them to optimize compatibility with external diagnostic and memory analysis tools.
|
|
480
|
+
*/
|
|
481
|
+
export class IndirectCallRedirectorAccessor extends CallRedirectorAccessor {
|
|
482
|
+
mallocs = new Map(); // Map<address, size>
|
|
483
|
+
findMallocBlock(address) {
|
|
484
|
+
for (const [base, size] of this.mallocs.entries()) {
|
|
485
|
+
if (address >= base && address < base + size) {
|
|
486
|
+
return { base, size };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
/** Shared by query()/querySync() -- pure, no I/O, so no need for two copies. */
|
|
492
|
+
buildMallocBlockInfo(block) {
|
|
493
|
+
const info = new MemoryBasicInformation();
|
|
494
|
+
info.assign({
|
|
495
|
+
BaseAddress: block.base,
|
|
496
|
+
AllocationBase: block.base,
|
|
497
|
+
AllocationProtect: MemoryProtection.READWRITE,
|
|
498
|
+
PartitionId: 0,
|
|
499
|
+
RegionSize: BigInt(block.size),
|
|
500
|
+
State: MemoryState.COMMIT,
|
|
501
|
+
Protect: MemoryProtection.READWRITE,
|
|
502
|
+
Type: 0x20000, // MEM_PRIVATE
|
|
503
|
+
});
|
|
504
|
+
return info;
|
|
505
|
+
}
|
|
506
|
+
async alloc(size, address = null, protection = MemoryProtection.READWRITE, allocationType = MemoryState.COMMIT | MemoryState.RESERVE) {
|
|
507
|
+
// If it's standard READWRITE memory without a specific requested address, use malloc for maximum redirection!
|
|
508
|
+
if (protection === MemoryProtection.READWRITE &&
|
|
509
|
+
(address === null || address === undefined)) {
|
|
510
|
+
const result = await this.root.call(MsvcrtImpl.malloc, size);
|
|
511
|
+
if (!result || Number(result) === 0) {
|
|
512
|
+
throw new Error(`malloc failed in remote process for size ${size}`);
|
|
513
|
+
}
|
|
514
|
+
const allocatedAddr = Number(result);
|
|
515
|
+
this.mallocs.set(allocatedAddr, size);
|
|
516
|
+
return allocatedAddr;
|
|
517
|
+
}
|
|
518
|
+
// Otherwise, fall back to VirtualAlloc (e.g. for executable memory blocks)
|
|
519
|
+
return super.alloc(size, address, protection, allocationType);
|
|
520
|
+
}
|
|
521
|
+
allocSync(size, address = null, protection = MemoryProtection.READWRITE, allocationType = MemoryState.COMMIT | MemoryState.RESERVE) {
|
|
522
|
+
if (protection === MemoryProtection.READWRITE &&
|
|
523
|
+
(address === null || address === undefined)) {
|
|
524
|
+
const result = this.root.callSync(MsvcrtImpl.malloc, size);
|
|
525
|
+
if (!result || Number(result) === 0) {
|
|
526
|
+
throw new Error(`malloc failed in remote process for size ${size}`);
|
|
527
|
+
}
|
|
528
|
+
const allocatedAddr = Number(result);
|
|
529
|
+
this.mallocs.set(allocatedAddr, size);
|
|
530
|
+
return allocatedAddr;
|
|
531
|
+
}
|
|
532
|
+
return super.allocSync(size, address, protection, allocationType);
|
|
533
|
+
}
|
|
534
|
+
async free(address, size = 0, freeType = MemoryFreeType.RELEASE) {
|
|
535
|
+
const addrVal = Number(resolveAddress(address));
|
|
536
|
+
if (this.mallocs.has(addrVal)) {
|
|
537
|
+
await this.root.call(MsvcrtImpl.free, addrVal);
|
|
538
|
+
this.mallocs.delete(addrVal);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
return super.free(address, size, freeType);
|
|
542
|
+
}
|
|
543
|
+
freeSync(address, size = 0, freeType = MemoryFreeType.RELEASE) {
|
|
544
|
+
const addrVal = Number(resolveAddress(address));
|
|
545
|
+
if (this.mallocs.has(addrVal)) {
|
|
546
|
+
this.root.callSync(MsvcrtImpl.free, addrVal);
|
|
547
|
+
this.mallocs.delete(addrVal);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
return super.freeSync(address, size, freeType);
|
|
551
|
+
}
|
|
552
|
+
async protect(address, size, newProtect) {
|
|
553
|
+
const addrVal = Number(resolveAddress(address));
|
|
554
|
+
const block = this.findMallocBlock(addrVal);
|
|
555
|
+
if (block) {
|
|
556
|
+
if (newProtect !== MemoryProtection.READWRITE) {
|
|
557
|
+
throw new Error(`Cannot change protection of indirect heap block at 0x${addrVal.toString(16)} to ${newProtect}. Only READWRITE is allowed.`);
|
|
558
|
+
}
|
|
559
|
+
return MemoryProtection.READWRITE; // Already READWRITE
|
|
560
|
+
}
|
|
561
|
+
return super.protect(address, size, newProtect);
|
|
562
|
+
}
|
|
563
|
+
protectSync(address, size, newProtect) {
|
|
564
|
+
const addrVal = Number(resolveAddress(address));
|
|
565
|
+
const block = this.findMallocBlock(addrVal);
|
|
566
|
+
if (block) {
|
|
567
|
+
if (newProtect !== MemoryProtection.READWRITE) {
|
|
568
|
+
throw new Error(`Cannot change protection of indirect heap block at 0x${addrVal.toString(16)} to ${newProtect}. Only READWRITE is allowed.`);
|
|
569
|
+
}
|
|
570
|
+
return MemoryProtection.READWRITE;
|
|
571
|
+
}
|
|
572
|
+
return super.protectSync(address, size, newProtect);
|
|
573
|
+
}
|
|
574
|
+
async query(address) {
|
|
575
|
+
const addrVal = Number(resolveAddress(address));
|
|
576
|
+
const block = this.findMallocBlock(addrVal);
|
|
577
|
+
if (block) {
|
|
578
|
+
return this.buildMallocBlockInfo(block);
|
|
579
|
+
}
|
|
580
|
+
return super.query(address);
|
|
581
|
+
}
|
|
582
|
+
querySync(address) {
|
|
583
|
+
const addrVal = Number(resolveAddress(address));
|
|
584
|
+
const block = this.findMallocBlock(addrVal);
|
|
585
|
+
if (block) {
|
|
586
|
+
return this.buildMallocBlockInfo(block);
|
|
587
|
+
}
|
|
588
|
+
return super.querySync(address);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Middleware Accessor that automatically intercepts call arguments,
|
|
593
|
+
* marshals complex types (strings, buffers) to remotely allocated memory blocks,
|
|
594
|
+
* executes the native call, and cleans up the temporary allocations.
|
|
595
|
+
*/
|
|
596
|
+
export class MarshallingCallableAccessor extends MiddlewareAccessor {
|
|
597
|
+
/**
|
|
598
|
+
* Shared by call()/callSync() -- pure decision of "does this argument need
|
|
599
|
+
* marshalling to a remote buffer, and if so, what bytes", with no I/O.
|
|
600
|
+
* Returns `null` for args that pass through unmodified.
|
|
601
|
+
*/
|
|
602
|
+
static bufferToMarshal(type, val) {
|
|
603
|
+
const normType = normalizeType(type);
|
|
604
|
+
if (normType === 'cstring' && typeof val === 'string') {
|
|
605
|
+
return Buffer.from(val + '\0', 'utf8');
|
|
606
|
+
}
|
|
607
|
+
else if (normType === 'cwstring' && typeof val === 'string') {
|
|
608
|
+
return Buffer.from(val + '\0', 'utf16le');
|
|
609
|
+
}
|
|
610
|
+
else if (val && typeof val === 'object' && Buffer.isBuffer(val)) {
|
|
611
|
+
return val;
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
async call(func, ...args) {
|
|
616
|
+
const modifiedArgs = [...args];
|
|
617
|
+
const tempAllocations = [];
|
|
618
|
+
try {
|
|
619
|
+
const signatureArgs = func.args || [];
|
|
620
|
+
for (let i = 0; i < signatureArgs.length; i++) {
|
|
621
|
+
const buf = MarshallingCallableAccessor.bufferToMarshal(signatureArgs[i], args[i]);
|
|
622
|
+
if (!buf)
|
|
623
|
+
continue;
|
|
624
|
+
const remoteAddr = Number(await this.root.alloc(buf.length));
|
|
625
|
+
await this.root.write(remoteAddr, buf);
|
|
626
|
+
tempAllocations.push({ address: remoteAddr, size: buf.length });
|
|
627
|
+
modifiedArgs[i] = remoteAddr;
|
|
628
|
+
}
|
|
629
|
+
return await this.backend.call(func, ...modifiedArgs);
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
for (const alloc of tempAllocations) {
|
|
633
|
+
try {
|
|
634
|
+
await this.root.free(alloc.address, alloc.size);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
// ignore cleanup errors
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
callSync(func, ...args) {
|
|
643
|
+
const modifiedArgs = [...args];
|
|
644
|
+
const tempAllocations = [];
|
|
645
|
+
try {
|
|
646
|
+
const signatureArgs = func.args || [];
|
|
647
|
+
for (let i = 0; i < signatureArgs.length; i++) {
|
|
648
|
+
const buf = MarshallingCallableAccessor.bufferToMarshal(signatureArgs[i], args[i]);
|
|
649
|
+
if (!buf)
|
|
650
|
+
continue;
|
|
651
|
+
const remoteAddr = Number(this.root.allocSync(buf.length));
|
|
652
|
+
this.root.writeSync(remoteAddr, buf);
|
|
653
|
+
tempAllocations.push({ address: remoteAddr, size: buf.length });
|
|
654
|
+
modifiedArgs[i] = remoteAddr;
|
|
655
|
+
}
|
|
656
|
+
return this.backend.callSync(func, ...modifiedArgs);
|
|
657
|
+
}
|
|
658
|
+
finally {
|
|
659
|
+
for (const alloc of tempAllocations) {
|
|
660
|
+
try {
|
|
661
|
+
this.root.freeSync(alloc.address, alloc.size);
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// ignore cleanup errors
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Middleware Accessor that redirects write operations to run inside the target process's context
|
|
672
|
+
* by executing msvcrt!memset calls. This is useful to optimize in-process memory initialization and avoid cross-process handle locks.
|
|
673
|
+
* It optimizes writes by scanning the buffer for contiguous identical byte blocks
|
|
674
|
+
* and writing them with a single memset call.
|
|
675
|
+
*/
|
|
676
|
+
export class MemsetWriteAccessor extends MsvcrtDependentMiddlewareAccessor {
|
|
677
|
+
_isExecuting = false;
|
|
678
|
+
/**
|
|
679
|
+
* Shared by writeAfterInit()/writeAfterInitSync() -- pure run-length scan
|
|
680
|
+
* over the buffer, no I/O, so no need for two copies.
|
|
681
|
+
*/
|
|
682
|
+
static computeRuns(buffer) {
|
|
683
|
+
const runs = [];
|
|
684
|
+
let i = 0;
|
|
685
|
+
while (i < buffer.length) {
|
|
686
|
+
const val = buffer[i];
|
|
687
|
+
if (val === undefined)
|
|
688
|
+
break;
|
|
689
|
+
let runLength = 1;
|
|
690
|
+
while (i + runLength < buffer.length && buffer[i + runLength] === val) {
|
|
691
|
+
runLength++;
|
|
692
|
+
}
|
|
693
|
+
runs.push({ startOffset: i, value: val, count: runLength });
|
|
694
|
+
i += runLength;
|
|
695
|
+
}
|
|
696
|
+
return runs;
|
|
697
|
+
}
|
|
698
|
+
async writeAfterInit(address, data, offset = 0) {
|
|
699
|
+
if (this.isInitializing || this._isExecuting) {
|
|
700
|
+
return super.writeAfterInit(address, data, offset);
|
|
701
|
+
}
|
|
702
|
+
this._isExecuting = true;
|
|
703
|
+
try {
|
|
704
|
+
const baseAddress = resolveAddress(address) + offset;
|
|
705
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
|
706
|
+
const runs = MemsetWriteAccessor.computeRuns(buffer);
|
|
707
|
+
let totalWritten = 0;
|
|
708
|
+
for (const run of runs) {
|
|
709
|
+
const targetAddr = baseAddress + run.startOffset;
|
|
710
|
+
await this.root.call(MsvcrtImpl.memset, targetAddr, run.value, run.count);
|
|
711
|
+
totalWritten += run.count;
|
|
712
|
+
}
|
|
713
|
+
return totalWritten;
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
this._isExecuting = false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
writeAfterInitSync(address, data, offset = 0) {
|
|
720
|
+
if (this.isInitializingSync || this._isExecuting) {
|
|
721
|
+
return super.writeAfterInitSync(address, data, offset);
|
|
722
|
+
}
|
|
723
|
+
this._isExecuting = true;
|
|
724
|
+
try {
|
|
725
|
+
const baseAddress = resolveAddress(address) + offset;
|
|
726
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
|
727
|
+
const runs = MemsetWriteAccessor.computeRuns(buffer);
|
|
728
|
+
let totalWritten = 0;
|
|
729
|
+
for (const run of runs) {
|
|
730
|
+
const targetAddr = baseAddress + run.startOffset;
|
|
731
|
+
this.root.callSync(MsvcrtImpl.memset, targetAddr, run.value, run.count);
|
|
732
|
+
totalWritten += run.count;
|
|
733
|
+
}
|
|
734
|
+
return totalWritten;
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
this._isExecuting = false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Middleware Accessor that reads memory using remote memcmp binary search.
|
|
743
|
+
* This completely avoids ReadProcessMemory, making it extremely low-overhead
|
|
744
|
+
* and independent of any custom machineCode execution.
|
|
745
|
+
*/
|
|
746
|
+
export class MemcmpReadAccessor extends MsvcrtDependentMiddlewareAccessor {
|
|
747
|
+
_isExecuting = false;
|
|
748
|
+
async readAfterInit(address, size, offset = 0) {
|
|
749
|
+
if (this.isInitializing || this._isExecuting) {
|
|
750
|
+
return super.readAfterInit(address, size, offset);
|
|
751
|
+
}
|
|
752
|
+
this._isExecuting = true;
|
|
753
|
+
const baseAddress = resolveAddress(address) + offset;
|
|
754
|
+
const candidateAddr = Number(resolveAddress(await this.root.alloc(1)));
|
|
755
|
+
const candidateBuf = Buffer.alloc(1);
|
|
756
|
+
try {
|
|
757
|
+
const result = Buffer.alloc(size);
|
|
758
|
+
for (let i = 0; i < size; i++) {
|
|
759
|
+
let low = 0;
|
|
760
|
+
let high = 255;
|
|
761
|
+
let foundByte = 0;
|
|
762
|
+
while (low <= high) {
|
|
763
|
+
const mid = (low + high) >> 1;
|
|
764
|
+
candidateBuf[0] = mid;
|
|
765
|
+
await this.root.write(candidateAddr, candidateBuf);
|
|
766
|
+
const cmp = await this.root.call(MsvcrtImpl.memcmp, baseAddress + i, candidateAddr, 1);
|
|
767
|
+
const cmpVal = Number(cmp);
|
|
768
|
+
if (cmpVal === 0) {
|
|
769
|
+
foundByte = mid;
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
else if (cmpVal < 0) {
|
|
773
|
+
high = mid - 1;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
low = mid + 1;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
result[i] = foundByte;
|
|
780
|
+
}
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
this._isExecuting = false;
|
|
785
|
+
await this.root.free(candidateAddr).catch(() => { });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
readAfterInitSync(address, size, offset = 0) {
|
|
789
|
+
if (this.isInitializingSync || this._isExecuting) {
|
|
790
|
+
return super.readAfterInitSync(address, size, offset);
|
|
791
|
+
}
|
|
792
|
+
this._isExecuting = true;
|
|
793
|
+
const baseAddress = resolveAddress(address) + offset;
|
|
794
|
+
const candidateAddr = Number(resolveAddress(this.root.allocSync(1)));
|
|
795
|
+
const candidateBuf = Buffer.alloc(1);
|
|
796
|
+
try {
|
|
797
|
+
const result = Buffer.alloc(size);
|
|
798
|
+
for (let i = 0; i < size; i++) {
|
|
799
|
+
let low = 0;
|
|
800
|
+
let high = 255;
|
|
801
|
+
let foundByte = 0;
|
|
802
|
+
while (low <= high) {
|
|
803
|
+
const mid = (low + high) >> 1;
|
|
804
|
+
candidateBuf[0] = mid;
|
|
805
|
+
this.root.writeSync(candidateAddr, candidateBuf);
|
|
806
|
+
const cmp = this.root.callSync(MsvcrtImpl.memcmp, baseAddress + i, candidateAddr, 1);
|
|
807
|
+
const cmpVal = Number(cmp);
|
|
808
|
+
if (cmpVal === 0) {
|
|
809
|
+
foundByte = mid;
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
else if (cmpVal < 0) {
|
|
813
|
+
high = mid - 1;
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
low = mid + 1;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
result[i] = foundByte;
|
|
820
|
+
}
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
this._isExecuting = false;
|
|
825
|
+
try {
|
|
826
|
+
this.root.freeSync(candidateAddr);
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
/* ignore cleanup errors */
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Dead-end fallback accessor that throws an error for every single memory operation.
|
|
836
|
+
* Used as a strict base definition or safety guard for middleware accessor pipelines
|
|
837
|
+
* to ensure unsupported operations are explicitly caught rather than silently ignored.
|
|
838
|
+
*/
|
|
839
|
+
export class ThrowingMemoryAccessor {
|
|
840
|
+
isLocal = false;
|
|
841
|
+
processId;
|
|
842
|
+
constructor(processId) {
|
|
843
|
+
this.processId = processId;
|
|
844
|
+
}
|
|
845
|
+
throwError(method) {
|
|
846
|
+
throw new Error(`ThrowingMemoryAccessor: '${method}' is not implemented or permitted.`);
|
|
847
|
+
}
|
|
848
|
+
enableDebug() { }
|
|
849
|
+
disableDebug() { }
|
|
850
|
+
async read(_address, _size, _offset = 0) {
|
|
851
|
+
this.throwError('read');
|
|
852
|
+
}
|
|
853
|
+
readSync(_address, _size, _offset = 0) {
|
|
854
|
+
this.throwError('readSync');
|
|
855
|
+
}
|
|
856
|
+
async write(_address, _data, _offset = 0) {
|
|
857
|
+
this.throwError('write');
|
|
858
|
+
}
|
|
859
|
+
writeSync(_address, _data, _offset = 0) {
|
|
860
|
+
this.throwError('writeSync');
|
|
861
|
+
}
|
|
862
|
+
async alloc(_size, _address = null, _protection, _allocationType) {
|
|
863
|
+
this.throwError('alloc');
|
|
864
|
+
}
|
|
865
|
+
allocSync(_size, _address = null, _protection, _allocationType) {
|
|
866
|
+
this.throwError('allocSync');
|
|
867
|
+
}
|
|
868
|
+
async allocNear(_target, _size, _options) {
|
|
869
|
+
this.throwError('allocNear');
|
|
870
|
+
}
|
|
871
|
+
allocNearSync(_target, _size, _options) {
|
|
872
|
+
this.throwError('allocNearSync');
|
|
873
|
+
}
|
|
874
|
+
async free(_address, _size = 0, _freeType) {
|
|
875
|
+
this.throwError('free');
|
|
876
|
+
}
|
|
877
|
+
freeSync(_address, _size = 0, _freeType) {
|
|
878
|
+
this.throwError('freeSync');
|
|
879
|
+
}
|
|
880
|
+
async protect(_address, _size, _newProtect) {
|
|
881
|
+
this.throwError('protect');
|
|
882
|
+
}
|
|
883
|
+
protectSync(_address, _size, _newProtect) {
|
|
884
|
+
this.throwError('protectSync');
|
|
885
|
+
}
|
|
886
|
+
async query(_address) {
|
|
887
|
+
this.throwError('query');
|
|
888
|
+
}
|
|
889
|
+
querySync(_address) {
|
|
890
|
+
this.throwError('querySync');
|
|
891
|
+
}
|
|
892
|
+
async call(_func, ..._args) {
|
|
893
|
+
this.throwError('call');
|
|
894
|
+
}
|
|
895
|
+
callSync(_func, ..._args) {
|
|
896
|
+
this.throwError('callSync');
|
|
897
|
+
}
|
|
898
|
+
// eslint-disable-next-line require-yield
|
|
899
|
+
async *scan(_address, _size, _pattern) {
|
|
900
|
+
this.throwError('scan');
|
|
901
|
+
}
|
|
902
|
+
// eslint-disable-next-line require-yield
|
|
903
|
+
*scanSync(_address, _size, _pattern) {
|
|
904
|
+
this.throwError('scanSync');
|
|
905
|
+
}
|
|
906
|
+
async readInt8(_address, _offset = 0) {
|
|
907
|
+
this.throwError('readInt8');
|
|
908
|
+
}
|
|
909
|
+
readInt8Sync(_address, _offset = 0) {
|
|
910
|
+
this.throwError('readInt8Sync');
|
|
911
|
+
}
|
|
912
|
+
async readUInt8(_address, _offset = 0) {
|
|
913
|
+
this.throwError('readUInt8');
|
|
914
|
+
}
|
|
915
|
+
readUInt8Sync(_address, _offset = 0) {
|
|
916
|
+
this.throwError('readUInt8Sync');
|
|
917
|
+
}
|
|
918
|
+
async readInt16(_address, _offset = 0) {
|
|
919
|
+
this.throwError('readInt16');
|
|
920
|
+
}
|
|
921
|
+
readInt16Sync(_address, _offset = 0) {
|
|
922
|
+
this.throwError('readInt16Sync');
|
|
923
|
+
}
|
|
924
|
+
async readUInt16(_address, _offset = 0) {
|
|
925
|
+
this.throwError('readUInt16');
|
|
926
|
+
}
|
|
927
|
+
readUInt16Sync(_address, _offset = 0) {
|
|
928
|
+
this.throwError('readUInt16Sync');
|
|
929
|
+
}
|
|
930
|
+
async readInt32(_address, _offset = 0) {
|
|
931
|
+
this.throwError('readInt32');
|
|
932
|
+
}
|
|
933
|
+
readInt32Sync(_address, _offset = 0) {
|
|
934
|
+
this.throwError('readInt32Sync');
|
|
935
|
+
}
|
|
936
|
+
async readUInt32(_address, _offset = 0) {
|
|
937
|
+
this.throwError('readUInt32');
|
|
938
|
+
}
|
|
939
|
+
readUInt32Sync(_address, _offset = 0) {
|
|
940
|
+
this.throwError('readUInt32Sync');
|
|
941
|
+
}
|
|
942
|
+
async readInt64(_address, _offset = 0) {
|
|
943
|
+
this.throwError('readInt64');
|
|
944
|
+
}
|
|
945
|
+
readInt64Sync(_address, _offset = 0) {
|
|
946
|
+
this.throwError('readInt64Sync');
|
|
947
|
+
}
|
|
948
|
+
async readUInt64(_address, _offset = 0) {
|
|
949
|
+
this.throwError('readUInt64');
|
|
950
|
+
}
|
|
951
|
+
readUInt64Sync(_address, _offset = 0) {
|
|
952
|
+
this.throwError('readUInt64Sync');
|
|
953
|
+
}
|
|
954
|
+
async readFloat(_address, _offset = 0) {
|
|
955
|
+
this.throwError('readFloat');
|
|
956
|
+
}
|
|
957
|
+
readFloatSync(_address, _offset = 0) {
|
|
958
|
+
this.throwError('readFloatSync');
|
|
959
|
+
}
|
|
960
|
+
async readDouble(_address, _offset = 0) {
|
|
961
|
+
this.throwError('readDouble');
|
|
962
|
+
}
|
|
963
|
+
readDoubleSync(_address, _offset = 0) {
|
|
964
|
+
this.throwError('readDoubleSync');
|
|
965
|
+
}
|
|
966
|
+
async readPointer(_address, _offset = 0) {
|
|
967
|
+
this.throwError('readPointer');
|
|
968
|
+
}
|
|
969
|
+
readPointerSync(_address, _offset = 0) {
|
|
970
|
+
this.throwError('readPointerSync');
|
|
971
|
+
}
|
|
972
|
+
async writeInt8(_address, _value, _offset = 0) {
|
|
973
|
+
this.throwError('writeInt8');
|
|
974
|
+
}
|
|
975
|
+
writeInt8Sync(_address, _value, _offset = 0) {
|
|
976
|
+
this.throwError('writeInt8Sync');
|
|
977
|
+
}
|
|
978
|
+
async writeUInt8(_address, _value, _offset = 0) {
|
|
979
|
+
this.throwError('writeUInt8');
|
|
980
|
+
}
|
|
981
|
+
writeUInt8Sync(_address, _value, _offset = 0) {
|
|
982
|
+
this.throwError('writeUInt8Sync');
|
|
983
|
+
}
|
|
984
|
+
async writeInt16(_address, _value, _offset = 0) {
|
|
985
|
+
this.throwError('writeInt16');
|
|
986
|
+
}
|
|
987
|
+
writeInt16Sync(_address, _value, _offset = 0) {
|
|
988
|
+
this.throwError('writeInt16Sync');
|
|
989
|
+
}
|
|
990
|
+
async writeUInt16(_address, _value, _offset = 0) {
|
|
991
|
+
this.throwError('writeUInt16');
|
|
992
|
+
}
|
|
993
|
+
writeUInt16Sync(_address, _value, _offset = 0) {
|
|
994
|
+
this.throwError('writeUInt16Sync');
|
|
995
|
+
}
|
|
996
|
+
async writeInt32(_address, _value, _offset = 0) {
|
|
997
|
+
this.throwError('writeInt32');
|
|
998
|
+
}
|
|
999
|
+
writeInt32Sync(_address, _value, _offset = 0) {
|
|
1000
|
+
this.throwError('writeInt32Sync');
|
|
1001
|
+
}
|
|
1002
|
+
async writeUInt32(_address, _value, _offset = 0) {
|
|
1003
|
+
this.throwError('writeUInt32');
|
|
1004
|
+
}
|
|
1005
|
+
writeUInt32Sync(_address, _value, _offset = 0) {
|
|
1006
|
+
this.throwError('writeUInt32Sync');
|
|
1007
|
+
}
|
|
1008
|
+
async writeInt64(_address, _value, _offset = 0) {
|
|
1009
|
+
this.throwError('writeInt64');
|
|
1010
|
+
}
|
|
1011
|
+
writeInt64Sync(_address, _value, _offset = 0) {
|
|
1012
|
+
this.throwError('writeInt64Sync');
|
|
1013
|
+
}
|
|
1014
|
+
async writeUInt64(_address, _value, _offset = 0) {
|
|
1015
|
+
this.throwError('writeUInt64');
|
|
1016
|
+
}
|
|
1017
|
+
writeUInt64Sync(_address, _value, _offset = 0) {
|
|
1018
|
+
this.throwError('writeUInt64Sync');
|
|
1019
|
+
}
|
|
1020
|
+
async writeFloat(_address, _value, _offset = 0) {
|
|
1021
|
+
this.throwError('writeFloat');
|
|
1022
|
+
}
|
|
1023
|
+
writeFloatSync(_address, _value, _offset = 0) {
|
|
1024
|
+
this.throwError('writeFloatSync');
|
|
1025
|
+
}
|
|
1026
|
+
async writeDouble(_address, _value, _offset = 0) {
|
|
1027
|
+
this.throwError('writeDouble');
|
|
1028
|
+
}
|
|
1029
|
+
writeDoubleSync(_address, _value, _offset = 0) {
|
|
1030
|
+
this.throwError('writeDoubleSync');
|
|
1031
|
+
}
|
|
1032
|
+
async writePointer(_address, _value, _offset = 0) {
|
|
1033
|
+
this.throwError('writePointer');
|
|
1034
|
+
}
|
|
1035
|
+
writePointerSync(_address, _value, _offset = 0) {
|
|
1036
|
+
this.throwError('writePointerSync');
|
|
1037
|
+
}
|
|
1038
|
+
async machineCode(_machineCode) {
|
|
1039
|
+
this.throwError('machineCode');
|
|
1040
|
+
}
|
|
1041
|
+
machineCodeSync(_machineCode) {
|
|
1042
|
+
this.throwError('machineCodeSync');
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Middleware Accessor that redirects memory reads to transfer via a temporary file,
|
|
1047
|
+
* using standard C library calls (fopen, fwrite, fclose) in the target process.
|
|
1048
|
+
* This is useful to perform high-volume structured memory extraction through file I/O instead of raw memory streaming.
|
|
1049
|
+
*/
|
|
1050
|
+
export class FileTransferReadAccessor extends MsvcrtDependentMiddlewareAccessor {
|
|
1051
|
+
// The temp file is opened ONCE (fopen "wb") and the FILE* reused for every
|
|
1052
|
+
// transfer. Opening is lazy (first transfer) rather than in onInit: during
|
|
1053
|
+
// the init cascade the upper chain isn't ready yet, so a remote fopen/free
|
|
1054
|
+
// there triggers nested re-init and deadlocks the redirect thread. By first
|
|
1055
|
+
// transfer the chain is fully up. The path/mode buffers are written, fed to
|
|
1056
|
+
// fopen and freed immediately -- no per-transfer path re-spelling (the
|
|
1057
|
+
// dominant redirection overhead) and no lingering remote buffers. Closed in onDeinit.
|
|
1058
|
+
hFile;
|
|
1059
|
+
tempFilePath;
|
|
1060
|
+
async onInit() {
|
|
1061
|
+
await super.onInit();
|
|
1062
|
+
this.tempFilePath = path.join(os.tmpdir(), `exoproc-read-${randomUUID()}.tmp`);
|
|
1063
|
+
}
|
|
1064
|
+
onInitSync() {
|
|
1065
|
+
super.onInitSync();
|
|
1066
|
+
this.tempFilePath = path.join(os.tmpdir(), `exoproc-read-${randomUUID()}.tmp`);
|
|
1067
|
+
}
|
|
1068
|
+
// Every internal call below goes through `this.backend` (the next stage
|
|
1069
|
+
// down the chain), never `this.root` -- `this.root` climbs back to the
|
|
1070
|
+
// *top* of the chain and back down through this very accessor, so a plain
|
|
1071
|
+
// plumbing write (the fopen path/mode strings) would re-enter
|
|
1072
|
+
// readAfterInit()/readAfterInitSync() while it's already running. Routing
|
|
1073
|
+
// through `this.backend` only ever moves further down the chain, so it
|
|
1074
|
+
// can't loop back into this class (or any class above it) at all --
|
|
1075
|
+
// no reentrancy is possible, so no reentrancy flag is needed either.
|
|
1076
|
+
async ensureOpen() {
|
|
1077
|
+
if (this.hFile)
|
|
1078
|
+
return;
|
|
1079
|
+
const pathBuf = Buffer.from(this.tempFilePath + '\0', 'utf8');
|
|
1080
|
+
const modeBuf = Buffer.from('wb\0', 'utf8');
|
|
1081
|
+
const pathPtr = await this.backend.alloc(pathBuf.length);
|
|
1082
|
+
const modePtr = await this.backend.alloc(modeBuf.length);
|
|
1083
|
+
await this.backend.write(pathPtr, pathBuf);
|
|
1084
|
+
await this.backend.write(modePtr, modeBuf);
|
|
1085
|
+
this.hFile = await this.backend.call(MsvcrtImpl.fopen, pathPtr, modePtr);
|
|
1086
|
+
// fopen copied the path/mode strings, so the buffers can go right away.
|
|
1087
|
+
await this.backend.free(pathPtr).catch(() => { });
|
|
1088
|
+
await this.backend.free(modePtr).catch(() => { });
|
|
1089
|
+
if (!this.hFile || Number(this.hFile) === 0) {
|
|
1090
|
+
this.hFile = undefined;
|
|
1091
|
+
throw new Error(`fopen failed to open temp file in write mode: ${this.tempFilePath}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
ensureOpenSync() {
|
|
1095
|
+
if (this.hFile)
|
|
1096
|
+
return;
|
|
1097
|
+
const pathBuf = Buffer.from(this.tempFilePath + '\0', 'utf8');
|
|
1098
|
+
const modeBuf = Buffer.from('wb\0', 'utf8');
|
|
1099
|
+
const pathPtr = this.backend.allocSync(pathBuf.length);
|
|
1100
|
+
const modePtr = this.backend.allocSync(modeBuf.length);
|
|
1101
|
+
this.backend.writeSync(pathPtr, pathBuf);
|
|
1102
|
+
this.backend.writeSync(modePtr, modeBuf);
|
|
1103
|
+
this.hFile = this.backend.callSync(MsvcrtImpl.fopen, pathPtr, modePtr);
|
|
1104
|
+
try {
|
|
1105
|
+
this.backend.freeSync(pathPtr);
|
|
1106
|
+
}
|
|
1107
|
+
catch {
|
|
1108
|
+
/* ignore cleanup errors */
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
this.backend.freeSync(modePtr);
|
|
1112
|
+
}
|
|
1113
|
+
catch {
|
|
1114
|
+
/* ignore cleanup errors */
|
|
1115
|
+
}
|
|
1116
|
+
if (!this.hFile || Number(this.hFile) === 0) {
|
|
1117
|
+
this.hFile = undefined;
|
|
1118
|
+
throw new Error(`fopen failed to open temp file in write mode: ${this.tempFilePath}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async onDeinit() {
|
|
1122
|
+
// Close via `this.backend` (not `this.root`): during teardown the chain
|
|
1123
|
+
// deinits top-down, so `this.root` is already deinitialized here and a
|
|
1124
|
+
// root.call would re-init the whole redirect chain via call()'s init guard.
|
|
1125
|
+
// `this.backend` is still initialized at onDeinit time (it deinits after us).
|
|
1126
|
+
if (this.hFile) {
|
|
1127
|
+
await this.backend.call(MsvcrtImpl.fclose, this.hFile).catch(() => { });
|
|
1128
|
+
this.hFile = undefined;
|
|
1129
|
+
}
|
|
1130
|
+
if (this.tempFilePath) {
|
|
1131
|
+
try {
|
|
1132
|
+
fs.unlinkSync(this.tempFilePath);
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
/* ignore cleanup errors */
|
|
1136
|
+
}
|
|
1137
|
+
this.tempFilePath = undefined;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
onDeinitSync() {
|
|
1141
|
+
if (this.hFile) {
|
|
1142
|
+
try {
|
|
1143
|
+
this.backend.callSync(MsvcrtImpl.fclose, this.hFile);
|
|
1144
|
+
}
|
|
1145
|
+
catch {
|
|
1146
|
+
/* ignore cleanup errors */
|
|
1147
|
+
}
|
|
1148
|
+
this.hFile = undefined;
|
|
1149
|
+
}
|
|
1150
|
+
if (this.tempFilePath) {
|
|
1151
|
+
try {
|
|
1152
|
+
fs.unlinkSync(this.tempFilePath);
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
/* ignore cleanup errors */
|
|
1156
|
+
}
|
|
1157
|
+
this.tempFilePath = undefined;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async readAfterInit(address, size, offset = 0) {
|
|
1161
|
+
if (this.isInitializing || !this.tempFilePath) {
|
|
1162
|
+
return super.readAfterInit(address, size, offset);
|
|
1163
|
+
}
|
|
1164
|
+
await this.ensureOpen();
|
|
1165
|
+
const targetAddr = resolveAddress(address) + offset;
|
|
1166
|
+
// Rewind so we overwrite from the start, dump target memory into the
|
|
1167
|
+
// file, then flush so the local read below sees the fresh bytes.
|
|
1168
|
+
await this.backend.call(MsvcrtImpl.rewind, this.hFile);
|
|
1169
|
+
const written = await this.backend.call(MsvcrtImpl.fwrite, targetAddr, 1, size, this.hFile);
|
|
1170
|
+
if (Number(written) !== size) {
|
|
1171
|
+
throw new Error(`fwrite failed to write entire memory block of size ${size} (wrote ${written})`);
|
|
1172
|
+
}
|
|
1173
|
+
await this.backend.call(MsvcrtImpl.fflush, this.hFile);
|
|
1174
|
+
// The file may still carry trailing bytes from a larger previous
|
|
1175
|
+
// transfer, so take only the `size` bytes we just wrote.
|
|
1176
|
+
return fs.readFileSync(this.tempFilePath).subarray(0, size);
|
|
1177
|
+
}
|
|
1178
|
+
readAfterInitSync(address, size, offset = 0) {
|
|
1179
|
+
if (this.isInitializingSync || !this.tempFilePath) {
|
|
1180
|
+
return super.readAfterInitSync(address, size, offset);
|
|
1181
|
+
}
|
|
1182
|
+
this.ensureOpenSync();
|
|
1183
|
+
const targetAddr = resolveAddress(address) + offset;
|
|
1184
|
+
this.backend.callSync(MsvcrtImpl.rewind, this.hFile);
|
|
1185
|
+
const written = this.backend.callSync(MsvcrtImpl.fwrite, targetAddr, 1, size, this.hFile);
|
|
1186
|
+
if (Number(written) !== size) {
|
|
1187
|
+
throw new Error(`fwrite failed to write entire memory block of size ${size} (wrote ${written})`);
|
|
1188
|
+
}
|
|
1189
|
+
this.backend.callSync(MsvcrtImpl.fflush, this.hFile);
|
|
1190
|
+
return fs.readFileSync(this.tempFilePath).subarray(0, size);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Middleware Accessor that redirects memory writes to transfer via a temporary file,
|
|
1195
|
+
* using standard C library calls (fopen, fread, fclose) in the target process.
|
|
1196
|
+
* This is useful to perform structured memory staging via file systems to handle large block updates safely.
|
|
1197
|
+
*/
|
|
1198
|
+
export class FileTransferWriteAccessor extends MsvcrtDependentMiddlewareAccessor {
|
|
1199
|
+
// The temp file is opened ONCE in onInit (fopen "rb") and the FILE* reused for
|
|
1200
|
+
// every transfer (see FileTransferReadAccessor). Each write drops the payload
|
|
1201
|
+
// into the file locally, rewinds the shared handle, and freads it into target.
|
|
1202
|
+
hFile;
|
|
1203
|
+
tempFilePath;
|
|
1204
|
+
async onInit() {
|
|
1205
|
+
await super.onInit();
|
|
1206
|
+
this.tempFilePath = path.join(os.tmpdir(), `exoproc-write-${randomUUID()}.tmp`);
|
|
1207
|
+
}
|
|
1208
|
+
onInitSync() {
|
|
1209
|
+
super.onInitSync();
|
|
1210
|
+
this.tempFilePath = path.join(os.tmpdir(), `exoproc-write-${randomUUID()}.tmp`);
|
|
1211
|
+
}
|
|
1212
|
+
// Lazy open on first transfer -- see FileTransferReadAccessor.ensureOpen for
|
|
1213
|
+
// why this can't run in onInit. The temp file must already exist on disk (the
|
|
1214
|
+
// caller writes the payload before calling this) so fopen("rb") succeeds.
|
|
1215
|
+
//
|
|
1216
|
+
// Every internal call below goes through `this.backend` (the next stage
|
|
1217
|
+
// down the chain), never `this.root` -- `this.root` climbs back to the
|
|
1218
|
+
// *top* of the chain and back down through this very accessor, so a plain
|
|
1219
|
+
// plumbing write (the fopen path/mode strings) would re-enter
|
|
1220
|
+
// writeAfterInit()/writeAfterInitSync() while it's already running. Routing
|
|
1221
|
+
// through `this.backend` only ever moves further down the chain, so it
|
|
1222
|
+
// can't loop back into this class (or any class above it) at all -- no
|
|
1223
|
+
// reentrancy is possible, so no reentrancy flag is needed either.
|
|
1224
|
+
async ensureOpen() {
|
|
1225
|
+
if (this.hFile)
|
|
1226
|
+
return;
|
|
1227
|
+
const pathBuf = Buffer.from(this.tempFilePath + '\0', 'utf8');
|
|
1228
|
+
const modeBuf = Buffer.from('rb\0', 'utf8');
|
|
1229
|
+
const pathPtr = await this.backend.alloc(pathBuf.length);
|
|
1230
|
+
const modePtr = await this.backend.alloc(modeBuf.length);
|
|
1231
|
+
await this.backend.write(pathPtr, pathBuf);
|
|
1232
|
+
await this.backend.write(modePtr, modeBuf);
|
|
1233
|
+
this.hFile = await this.backend.call(MsvcrtImpl.fopen, pathPtr, modePtr);
|
|
1234
|
+
await this.backend.free(pathPtr).catch(() => { });
|
|
1235
|
+
await this.backend.free(modePtr).catch(() => { });
|
|
1236
|
+
if (!this.hFile || Number(this.hFile) === 0) {
|
|
1237
|
+
this.hFile = undefined;
|
|
1238
|
+
throw new Error(`fopen failed to open temp file in read mode: ${this.tempFilePath}`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
ensureOpenSync() {
|
|
1242
|
+
if (this.hFile)
|
|
1243
|
+
return;
|
|
1244
|
+
const pathBuf = Buffer.from(this.tempFilePath + '\0', 'utf8');
|
|
1245
|
+
const modeBuf = Buffer.from('rb\0', 'utf8');
|
|
1246
|
+
const pathPtr = this.backend.allocSync(pathBuf.length);
|
|
1247
|
+
const modePtr = this.backend.allocSync(modeBuf.length);
|
|
1248
|
+
this.backend.writeSync(pathPtr, pathBuf);
|
|
1249
|
+
this.backend.writeSync(modePtr, modeBuf);
|
|
1250
|
+
this.hFile = this.backend.callSync(MsvcrtImpl.fopen, pathPtr, modePtr);
|
|
1251
|
+
try {
|
|
1252
|
+
this.backend.freeSync(pathPtr);
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
/* ignore cleanup errors */
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
this.backend.freeSync(modePtr);
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
/* ignore cleanup errors */
|
|
1262
|
+
}
|
|
1263
|
+
if (!this.hFile || Number(this.hFile) === 0) {
|
|
1264
|
+
this.hFile = undefined;
|
|
1265
|
+
throw new Error(`fopen failed to open temp file in read mode: ${this.tempFilePath}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async onDeinit() {
|
|
1269
|
+
// Close via `this.backend` -- see FileTransferReadAccessor.onDeinit.
|
|
1270
|
+
if (this.hFile) {
|
|
1271
|
+
await this.backend.call(MsvcrtImpl.fclose, this.hFile).catch(() => { });
|
|
1272
|
+
this.hFile = undefined;
|
|
1273
|
+
}
|
|
1274
|
+
if (this.tempFilePath) {
|
|
1275
|
+
try {
|
|
1276
|
+
fs.unlinkSync(this.tempFilePath);
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
/* ignore cleanup errors */
|
|
1280
|
+
}
|
|
1281
|
+
this.tempFilePath = undefined;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
onDeinitSync() {
|
|
1285
|
+
if (this.hFile) {
|
|
1286
|
+
try {
|
|
1287
|
+
this.backend.callSync(MsvcrtImpl.fclose, this.hFile);
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
/* ignore cleanup errors */
|
|
1291
|
+
}
|
|
1292
|
+
this.hFile = undefined;
|
|
1293
|
+
}
|
|
1294
|
+
if (this.tempFilePath) {
|
|
1295
|
+
try {
|
|
1296
|
+
fs.unlinkSync(this.tempFilePath);
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
/* ignore cleanup errors */
|
|
1300
|
+
}
|
|
1301
|
+
this.tempFilePath = undefined;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
async writeAfterInit(address, data, offset = 0) {
|
|
1305
|
+
if (this.isInitializing || !this.tempFilePath) {
|
|
1306
|
+
return super.writeAfterInit(address, data, offset);
|
|
1307
|
+
}
|
|
1308
|
+
const targetAddr = resolveAddress(address) + offset;
|
|
1309
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
|
1310
|
+
// Put the payload in the file (local, truncating write) BEFORE opening so
|
|
1311
|
+
// fopen("rb") sees a valid file, then rewind the shared FILE* so fread
|
|
1312
|
+
// re-reads the freshly written bytes from offset 0.
|
|
1313
|
+
fs.writeFileSync(this.tempFilePath, buffer);
|
|
1314
|
+
await this.ensureOpen();
|
|
1315
|
+
await this.backend.call(MsvcrtImpl.rewind, this.hFile);
|
|
1316
|
+
const readBytes = await this.backend.call(MsvcrtImpl.fread, targetAddr, 1, buffer.length, this.hFile);
|
|
1317
|
+
if (Number(readBytes) !== buffer.length) {
|
|
1318
|
+
throw new Error(`fread failed to read entire file block of size ${buffer.length} (read ${readBytes})`);
|
|
1319
|
+
}
|
|
1320
|
+
return Number(readBytes);
|
|
1321
|
+
}
|
|
1322
|
+
writeAfterInitSync(address, data, offset = 0) {
|
|
1323
|
+
if (this.isInitializingSync || !this.tempFilePath) {
|
|
1324
|
+
return super.writeAfterInitSync(address, data, offset);
|
|
1325
|
+
}
|
|
1326
|
+
const targetAddr = resolveAddress(address) + offset;
|
|
1327
|
+
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
|
1328
|
+
fs.writeFileSync(this.tempFilePath, buffer);
|
|
1329
|
+
this.ensureOpenSync();
|
|
1330
|
+
this.backend.callSync(MsvcrtImpl.rewind, this.hFile);
|
|
1331
|
+
const readBytes = this.backend.callSync(MsvcrtImpl.fread, targetAddr, 1, buffer.length, this.hFile);
|
|
1332
|
+
if (Number(readBytes) !== buffer.length) {
|
|
1333
|
+
throw new Error(`fread failed to read entire file block of size ${buffer.length} (read ${readBytes})`);
|
|
1334
|
+
}
|
|
1335
|
+
return Number(readBytes);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Middleware Accessor that caches target process metadata (bitness, process name, core modules status)
|
|
1340
|
+
* on demand, and intercepts dynamic module queries to cache returned module handles.
|
|
1341
|
+
*/
|
|
1342
|
+
export class ProcessCacheAccessor extends InittableMiddlewareAccessor {
|
|
1343
|
+
static TARGET_MODULE_NAMES = new Set([
|
|
1344
|
+
'msvcrt',
|
|
1345
|
+
'msvcrt.dll',
|
|
1346
|
+
'user32',
|
|
1347
|
+
'user32.dll',
|
|
1348
|
+
]);
|
|
1349
|
+
/** Pure, no I/O -- shared by every place that needs to trim/lowercase a module name buffer. */
|
|
1350
|
+
static extractModuleName(buf) {
|
|
1351
|
+
let len = buf.indexOf(0);
|
|
1352
|
+
if (len === -1)
|
|
1353
|
+
len = buf.length;
|
|
1354
|
+
return buf.subarray(0, len).toString('utf8').toLowerCase();
|
|
1355
|
+
}
|
|
1356
|
+
/** Pure, no I/O -- shared by callAfterInit()/callAfterInitSync(). */
|
|
1357
|
+
static resolveTrackedAddrs() {
|
|
1358
|
+
return {
|
|
1359
|
+
getModuleHandleExAAddr: resolveAddress(Kernel32Impl.GetModuleHandleExA.ptr),
|
|
1360
|
+
getModuleHandleAAddr: resolveAddress(Kernel32Impl.GetModuleHandleA.ptr),
|
|
1361
|
+
getCurrentProcessAddr: resolveAddress(Kernel32Impl.GetCurrentProcess.ptr),
|
|
1362
|
+
getCurrentProcessIdAddr: resolveAddress(Kernel32Impl.GetCurrentProcessId.ptr),
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
/** Pure, no I/O -- shared by getIs64Bit()/getIs64BitSync() to find an already-open handle in the chain. */
|
|
1366
|
+
static findOpenHandleInChain(backend) {
|
|
1367
|
+
let curr = backend;
|
|
1368
|
+
while (curr) {
|
|
1369
|
+
if (curr.handle !== undefined &&
|
|
1370
|
+
curr.handle !== null &&
|
|
1371
|
+
Number(curr.handle) !== 0) {
|
|
1372
|
+
return curr.handle;
|
|
1373
|
+
}
|
|
1374
|
+
curr = curr.backend;
|
|
1375
|
+
}
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
writeMemoryCache = new Map();
|
|
1379
|
+
moduleHandleCache = new Map();
|
|
1380
|
+
cachedIs64Bit = null;
|
|
1381
|
+
cachedProcessName = null;
|
|
1382
|
+
cachedCoreModules = null;
|
|
1383
|
+
async onInit() {
|
|
1384
|
+
// Empty JIT initialization since metadata is queried on demand
|
|
1385
|
+
}
|
|
1386
|
+
onInitSync() {
|
|
1387
|
+
// Empty JIT initialization since metadata is queried on demand
|
|
1388
|
+
}
|
|
1389
|
+
/** Core of getIs64Bit()/getIs64BitSync() -- entirely local/synchronous Win32 calls already, so both share this verbatim. */
|
|
1390
|
+
getIs64BitCore() {
|
|
1391
|
+
if (this.cachedIs64Bit !== null) {
|
|
1392
|
+
return this.cachedIs64Bit;
|
|
1393
|
+
}
|
|
1394
|
+
const handle = ProcessCacheAccessor.findOpenHandleInChain(this.backend);
|
|
1395
|
+
const hProcess = handle ||
|
|
1396
|
+
Kernel32Impl.OpenProcess(ProcessAccess.QUERY_INFORMATION | ProcessAccess.VM_READ, 0, this.processId);
|
|
1397
|
+
const ownsHandle = !handle && hProcess && Number(hProcess) !== 0;
|
|
1398
|
+
if (hProcess && Number(hProcess) !== 0) {
|
|
1399
|
+
try {
|
|
1400
|
+
const wow64Buf = Buffer.alloc(4);
|
|
1401
|
+
const success = Kernel32Impl.IsWow64Process(hProcess, wow64Buf);
|
|
1402
|
+
if (Number(success) !== 0) {
|
|
1403
|
+
const is32Bit = wow64Buf.readUInt32LE(0) !== 0;
|
|
1404
|
+
this.cachedIs64Bit = !is32Bit;
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
this.cachedIs64Bit = true; // Default to 64-bit on failure
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
finally {
|
|
1411
|
+
if (ownsHandle) {
|
|
1412
|
+
Kernel32Impl.CloseHandle(hProcess);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
this.cachedIs64Bit = true;
|
|
1418
|
+
}
|
|
1419
|
+
return this.cachedIs64Bit;
|
|
1420
|
+
}
|
|
1421
|
+
async getIs64Bit() {
|
|
1422
|
+
await this.init();
|
|
1423
|
+
return this.getIs64BitCore();
|
|
1424
|
+
}
|
|
1425
|
+
/** Sync twin of getIs64Bit() -- initializes the chain via initSync() if it hasn't run yet. */
|
|
1426
|
+
getIs64BitSync() {
|
|
1427
|
+
if (!this.isInitializingSync) {
|
|
1428
|
+
this.initSync();
|
|
1429
|
+
}
|
|
1430
|
+
return this.getIs64BitCore();
|
|
1431
|
+
}
|
|
1432
|
+
/** Core of getProcessName()/getProcessNameSync() -- entirely local/synchronous Win32 calls already, so both share this verbatim. */
|
|
1433
|
+
getProcessNameCore() {
|
|
1434
|
+
if (this.cachedProcessName !== null) {
|
|
1435
|
+
return this.cachedProcessName;
|
|
1436
|
+
}
|
|
1437
|
+
const hSnapshot = Kernel32Impl.CreateToolhelp32Snapshot(ToolhelpSnapshotFlag.PROCESS, 0);
|
|
1438
|
+
if (hSnapshot && Number(hSnapshot) !== 0 && Number(hSnapshot) !== -1) {
|
|
1439
|
+
try {
|
|
1440
|
+
const size = PROCESSENTRY32W_SIZE;
|
|
1441
|
+
const buf = Buffer.alloc(size);
|
|
1442
|
+
buf.writeUInt32LE(size, 0);
|
|
1443
|
+
let ok = Kernel32Impl.Process32FirstW(hSnapshot, buf);
|
|
1444
|
+
while (Number(ok) !== 0) {
|
|
1445
|
+
const pid = buf.readUInt32LE(8);
|
|
1446
|
+
if (pid === this.processId) {
|
|
1447
|
+
const nameBuf = buf.subarray(44, 44 + 520);
|
|
1448
|
+
let len = 0;
|
|
1449
|
+
for (let i = 0; i < nameBuf.length; i += 2) {
|
|
1450
|
+
if (nameBuf[i] === 0 && nameBuf[i + 1] === 0) {
|
|
1451
|
+
len = i;
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
this.cachedProcessName = nameBuf
|
|
1456
|
+
.subarray(0, len)
|
|
1457
|
+
.toString('utf16le');
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
buf.writeUInt32LE(size, 0);
|
|
1461
|
+
ok = Kernel32Impl.Process32NextW(hSnapshot, buf);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
finally {
|
|
1465
|
+
Kernel32Impl.CloseHandle(hSnapshot);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return this.cachedProcessName ?? '';
|
|
1469
|
+
}
|
|
1470
|
+
async getProcessName() {
|
|
1471
|
+
await this.init();
|
|
1472
|
+
return this.getProcessNameCore();
|
|
1473
|
+
}
|
|
1474
|
+
/** Sync twin of getProcessName() -- initializes the chain via initSync() if it hasn't run yet. */
|
|
1475
|
+
getProcessNameSync() {
|
|
1476
|
+
if (!this.isInitializingSync) {
|
|
1477
|
+
this.initSync();
|
|
1478
|
+
}
|
|
1479
|
+
return this.getProcessNameCore();
|
|
1480
|
+
}
|
|
1481
|
+
async getCoreModules() {
|
|
1482
|
+
await this.init();
|
|
1483
|
+
if (this.cachedCoreModules !== null) {
|
|
1484
|
+
return this.cachedCoreModules;
|
|
1485
|
+
}
|
|
1486
|
+
this.cachedCoreModules = await verifyCoreModules(this);
|
|
1487
|
+
return this.cachedCoreModules;
|
|
1488
|
+
}
|
|
1489
|
+
/** Sync twin of getCoreModules() -- initializes the chain via initSync() if it hasn't run yet. */
|
|
1490
|
+
getCoreModulesSync() {
|
|
1491
|
+
if (!this.isInitializingSync) {
|
|
1492
|
+
this.initSync();
|
|
1493
|
+
}
|
|
1494
|
+
if (this.cachedCoreModules !== null) {
|
|
1495
|
+
return this.cachedCoreModules;
|
|
1496
|
+
}
|
|
1497
|
+
this.cachedCoreModules = verifyCoreModulesSync(this);
|
|
1498
|
+
return this.cachedCoreModules;
|
|
1499
|
+
}
|
|
1500
|
+
async writeAfterInit(address, data, offset = 0) {
|
|
1501
|
+
const addrVal = resolveAddress(address) + offset;
|
|
1502
|
+
const buf = data instanceof Buffer ? data : Buffer.from(data);
|
|
1503
|
+
this.writeMemoryCache.set(addrVal, buf);
|
|
1504
|
+
return super.writeAfterInit(address, data, offset);
|
|
1505
|
+
}
|
|
1506
|
+
writeAfterInitSync(address, data, offset = 0) {
|
|
1507
|
+
const addrVal = resolveAddress(address) + offset;
|
|
1508
|
+
const buf = data instanceof Buffer ? data : Buffer.from(data);
|
|
1509
|
+
this.writeMemoryCache.set(addrVal, buf);
|
|
1510
|
+
return super.writeAfterInitSync(address, data, offset);
|
|
1511
|
+
}
|
|
1512
|
+
/** Cache lookup for a module name buffer at `namePtr` -- pure, no I/O; `null` means the caller must read it remotely. */
|
|
1513
|
+
cachedModuleName(namePtr) {
|
|
1514
|
+
const writtenBuf = this.writeMemoryCache.get(namePtr);
|
|
1515
|
+
return writtenBuf
|
|
1516
|
+
? ProcessCacheAccessor.extractModuleName(writtenBuf)
|
|
1517
|
+
: null;
|
|
1518
|
+
}
|
|
1519
|
+
async resolveModuleName(namePtr) {
|
|
1520
|
+
const cached = this.cachedModuleName(namePtr);
|
|
1521
|
+
if (cached !== null)
|
|
1522
|
+
return cached;
|
|
1523
|
+
try {
|
|
1524
|
+
const buf = await this.root.read(namePtr, 256);
|
|
1525
|
+
return ProcessCacheAccessor.extractModuleName(buf);
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
return '';
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
resolveModuleNameSync(namePtr) {
|
|
1532
|
+
const cached = this.cachedModuleName(namePtr);
|
|
1533
|
+
if (cached !== null)
|
|
1534
|
+
return cached;
|
|
1535
|
+
try {
|
|
1536
|
+
const buf = this.root.readSync(namePtr, 256);
|
|
1537
|
+
return ProcessCacheAccessor.extractModuleName(buf);
|
|
1538
|
+
}
|
|
1539
|
+
catch {
|
|
1540
|
+
return '';
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
async callAfterInit(func, args) {
|
|
1544
|
+
const funcAddr = resolveAddress(func.ptr);
|
|
1545
|
+
const { getModuleHandleExAAddr, getModuleHandleAAddr, getCurrentProcessAddr, getCurrentProcessIdAddr, } = ProcessCacheAccessor.resolveTrackedAddrs();
|
|
1546
|
+
if (funcAddr === getCurrentProcessAddr) {
|
|
1547
|
+
return 0xffffffffffffffffn;
|
|
1548
|
+
}
|
|
1549
|
+
else if (funcAddr === getCurrentProcessIdAddr) {
|
|
1550
|
+
return this.processId;
|
|
1551
|
+
}
|
|
1552
|
+
if (funcAddr === getModuleHandleExAAddr && args.length >= 3) {
|
|
1553
|
+
const flags = Number(args[0]);
|
|
1554
|
+
const namePtr = resolveAddress(args[1]);
|
|
1555
|
+
const outPtr = resolveAddress(args[2]);
|
|
1556
|
+
const isFromAddress = (flags & 4) !== 0;
|
|
1557
|
+
if (!isFromAddress && BigInt(namePtr) !== 0n) {
|
|
1558
|
+
const moduleName = await this.resolveModuleName(namePtr);
|
|
1559
|
+
const isTargetModule = ProcessCacheAccessor.TARGET_MODULE_NAMES.has(moduleName);
|
|
1560
|
+
if (isTargetModule && this.moduleHandleCache.has(moduleName)) {
|
|
1561
|
+
const cachedHModule = this.moduleHandleCache.get(moduleName);
|
|
1562
|
+
const hModuleBuf = Buffer.alloc(8);
|
|
1563
|
+
hModuleBuf.writeBigUInt64LE(cachedHModule, 0);
|
|
1564
|
+
await this.root.write(outPtr, hModuleBuf);
|
|
1565
|
+
return 1;
|
|
1566
|
+
}
|
|
1567
|
+
const result = await this.backend.call(func, args);
|
|
1568
|
+
if (isTargetModule && Number(result) !== 0) {
|
|
1569
|
+
const hModuleBuf = await this.root.read(outPtr, 8);
|
|
1570
|
+
const hModule = hModuleBuf.readBigUInt64LE(0);
|
|
1571
|
+
this.moduleHandleCache.set(moduleName, hModule);
|
|
1572
|
+
}
|
|
1573
|
+
return result;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
else if (funcAddr === getModuleHandleAAddr && args.length >= 1) {
|
|
1577
|
+
const namePtr = resolveAddress(args[0]);
|
|
1578
|
+
if (BigInt(namePtr) !== 0n) {
|
|
1579
|
+
const moduleName = await this.resolveModuleName(namePtr);
|
|
1580
|
+
const isTargetModule = ProcessCacheAccessor.TARGET_MODULE_NAMES.has(moduleName);
|
|
1581
|
+
if (isTargetModule && this.moduleHandleCache.has(moduleName)) {
|
|
1582
|
+
return this.moduleHandleCache.get(moduleName);
|
|
1583
|
+
}
|
|
1584
|
+
const result = await this.backend.call(func, args);
|
|
1585
|
+
if (isTargetModule && result && Number(result) !== 0) {
|
|
1586
|
+
this.moduleHandleCache.set(moduleName, BigInt(result));
|
|
1587
|
+
}
|
|
1588
|
+
return result;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return super.callAfterInit(func, args);
|
|
1592
|
+
}
|
|
1593
|
+
callAfterInitSync(func, args) {
|
|
1594
|
+
const funcAddr = resolveAddress(func.ptr);
|
|
1595
|
+
const { getModuleHandleExAAddr, getModuleHandleAAddr, getCurrentProcessAddr, getCurrentProcessIdAddr, } = ProcessCacheAccessor.resolveTrackedAddrs();
|
|
1596
|
+
if (funcAddr === getCurrentProcessAddr) {
|
|
1597
|
+
return 0xffffffffffffffffn;
|
|
1598
|
+
}
|
|
1599
|
+
else if (funcAddr === getCurrentProcessIdAddr) {
|
|
1600
|
+
return this.processId;
|
|
1601
|
+
}
|
|
1602
|
+
if (funcAddr === getModuleHandleExAAddr && args.length >= 3) {
|
|
1603
|
+
const flags = Number(args[0]);
|
|
1604
|
+
const namePtr = resolveAddress(args[1]);
|
|
1605
|
+
const outPtr = resolveAddress(args[2]);
|
|
1606
|
+
const isFromAddress = (flags & 4) !== 0;
|
|
1607
|
+
if (!isFromAddress && BigInt(namePtr) !== 0n) {
|
|
1608
|
+
const moduleName = this.resolveModuleNameSync(namePtr);
|
|
1609
|
+
const isTargetModule = ProcessCacheAccessor.TARGET_MODULE_NAMES.has(moduleName);
|
|
1610
|
+
if (isTargetModule && this.moduleHandleCache.has(moduleName)) {
|
|
1611
|
+
const cachedHModule = this.moduleHandleCache.get(moduleName);
|
|
1612
|
+
const hModuleBuf = Buffer.alloc(8);
|
|
1613
|
+
hModuleBuf.writeBigUInt64LE(cachedHModule, 0);
|
|
1614
|
+
this.root.writeSync(outPtr, hModuleBuf);
|
|
1615
|
+
return 1;
|
|
1616
|
+
}
|
|
1617
|
+
const result = this.backend.callSync(func, args);
|
|
1618
|
+
if (isTargetModule && Number(result) !== 0) {
|
|
1619
|
+
const hModuleBuf = this.root.readSync(outPtr, 8);
|
|
1620
|
+
const hModule = hModuleBuf.readBigUInt64LE(0);
|
|
1621
|
+
this.moduleHandleCache.set(moduleName, hModule);
|
|
1622
|
+
}
|
|
1623
|
+
return result;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
else if (funcAddr === getModuleHandleAAddr && args.length >= 1) {
|
|
1627
|
+
const namePtr = resolveAddress(args[0]);
|
|
1628
|
+
if (BigInt(namePtr) !== 0n) {
|
|
1629
|
+
const moduleName = this.resolveModuleNameSync(namePtr);
|
|
1630
|
+
const isTargetModule = ProcessCacheAccessor.TARGET_MODULE_NAMES.has(moduleName);
|
|
1631
|
+
if (isTargetModule && this.moduleHandleCache.has(moduleName)) {
|
|
1632
|
+
return this.moduleHandleCache.get(moduleName);
|
|
1633
|
+
}
|
|
1634
|
+
const result = this.backend.callSync(func, args);
|
|
1635
|
+
if (isTargetModule && result && Number(result) !== 0) {
|
|
1636
|
+
this.moduleHandleCache.set(moduleName, BigInt(result));
|
|
1637
|
+
}
|
|
1638
|
+
return result;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return super.callAfterInitSync(func, args);
|
|
1642
|
+
}
|
|
1643
|
+
async freeAfterInit(address, size, freeType) {
|
|
1644
|
+
const addrVal = resolveAddress(address);
|
|
1645
|
+
this.writeMemoryCache.delete(addrVal);
|
|
1646
|
+
return super.freeAfterInit(address, size, freeType);
|
|
1647
|
+
}
|
|
1648
|
+
freeAfterInitSync(address, size, freeType) {
|
|
1649
|
+
const addrVal = resolveAddress(address);
|
|
1650
|
+
this.writeMemoryCache.delete(addrVal);
|
|
1651
|
+
return super.freeAfterInitSync(address, size, freeType);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
export class MachineCodePoolMiddleware extends MiddlewareAccessor {
|
|
1655
|
+
blocks = [];
|
|
1656
|
+
alignment = DEFAULT_MACHINECODE_ALIGNMENT;
|
|
1657
|
+
defaultBlockSize = 4096;
|
|
1658
|
+
/**
|
|
1659
|
+
* Shared by getOrAllocBlock()/getOrAllocBlockSync() -- pure scan for
|
|
1660
|
+
* existing space, no I/O, so no need for two copies. `null` means the
|
|
1661
|
+
* caller must allocate a fresh block itself (the only part that needs I/O).
|
|
1662
|
+
*/
|
|
1663
|
+
findBlockWithSpace(needed) {
|
|
1664
|
+
for (let i = 0; i < this.blocks.length; i++) {
|
|
1665
|
+
const block = this.blocks[i];
|
|
1666
|
+
const alignedUsed = alignUp(block.used, this.alignment);
|
|
1667
|
+
if (block.size - alignedUsed >= needed) {
|
|
1668
|
+
return { address: block.address, offset: alignedUsed, blockIdx: i };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
async getOrAllocBlock(needed) {
|
|
1674
|
+
const existing = this.findBlockWithSpace(needed);
|
|
1675
|
+
if (existing)
|
|
1676
|
+
return existing;
|
|
1677
|
+
const blockSize = Math.max(this.defaultBlockSize, needed);
|
|
1678
|
+
const blockAddr = await this.root.alloc(blockSize, null, MemoryProtection.EXECUTE_READWRITE);
|
|
1679
|
+
const address = Number(resolveAddress(blockAddr));
|
|
1680
|
+
this.blocks.push({ address, size: blockSize, used: 0 });
|
|
1681
|
+
return { address, offset: 0, blockIdx: this.blocks.length - 1 };
|
|
1682
|
+
}
|
|
1683
|
+
getOrAllocBlockSync(needed) {
|
|
1684
|
+
const existing = this.findBlockWithSpace(needed);
|
|
1685
|
+
if (existing)
|
|
1686
|
+
return existing;
|
|
1687
|
+
const blockSize = Math.max(this.defaultBlockSize, needed);
|
|
1688
|
+
const blockAddr = this.root.allocSync(blockSize, null, MemoryProtection.EXECUTE_READWRITE);
|
|
1689
|
+
const address = Number(resolveAddress(blockAddr));
|
|
1690
|
+
this.blocks.push({ address, size: blockSize, used: 0 });
|
|
1691
|
+
return { address, offset: 0, blockIdx: this.blocks.length - 1 };
|
|
1692
|
+
}
|
|
1693
|
+
async machineCode(machineCode) {
|
|
1694
|
+
const size = machineCode.size;
|
|
1695
|
+
const bytes = Array.isArray(machineCode.bytes)
|
|
1696
|
+
? new Uint8Array(machineCode.bytes)
|
|
1697
|
+
: machineCode.bytes;
|
|
1698
|
+
const { address, offset, blockIdx } = await this.getOrAllocBlock(size);
|
|
1699
|
+
const targetAddr = address + offset;
|
|
1700
|
+
await this.root.write(targetAddr, bytes);
|
|
1701
|
+
this.blocks[blockIdx].used = offset + size;
|
|
1702
|
+
return targetAddr;
|
|
1703
|
+
}
|
|
1704
|
+
machineCodeSync(machineCode) {
|
|
1705
|
+
const size = machineCode.size;
|
|
1706
|
+
const bytes = Array.isArray(machineCode.bytes)
|
|
1707
|
+
? new Uint8Array(machineCode.bytes)
|
|
1708
|
+
: machineCode.bytes;
|
|
1709
|
+
const { address, offset, blockIdx } = this.getOrAllocBlockSync(size);
|
|
1710
|
+
const targetAddr = address + offset;
|
|
1711
|
+
this.root.writeSync(targetAddr, bytes);
|
|
1712
|
+
this.blocks[blockIdx].used = offset + size;
|
|
1713
|
+
return targetAddr;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
export class ScannerMiddleware extends MiddlewareAccessor {
|
|
1717
|
+
/**
|
|
1718
|
+
* Shared by scan()/scanSync() -- pure decision of which memmemN machineCode
|
|
1719
|
+
* to upload and how to encode the needle for a given pattern width, no I/O.
|
|
1720
|
+
* `sc` still needs uploading (await machineCode() vs machineCodeSync()) by
|
|
1721
|
+
* the caller; `needleVal`/`isWithoutBuffer` are used directly once uploaded.
|
|
1722
|
+
*/
|
|
1723
|
+
static pickMemmemVariant(n, patBytes) {
|
|
1724
|
+
if (n === 1) {
|
|
1725
|
+
return {
|
|
1726
|
+
sc: memmem1,
|
|
1727
|
+
needleVal: BigInt(patBytes.readUInt8(0)),
|
|
1728
|
+
isWithoutBuffer: true,
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
else if (n === 2) {
|
|
1732
|
+
return {
|
|
1733
|
+
sc: memmem2,
|
|
1734
|
+
needleVal: BigInt(patBytes.readUInt16LE(0)),
|
|
1735
|
+
isWithoutBuffer: true,
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
else if (n === 3 || n === 5 || n === 6 || n === 7) {
|
|
1739
|
+
let val = 0n;
|
|
1740
|
+
for (let i = 0; i < n; i++) {
|
|
1741
|
+
val |= BigInt(patBytes[i]) << BigInt(i * 8);
|
|
1742
|
+
}
|
|
1743
|
+
return { sc: memmemWithoutBuffer, needleVal: val, isWithoutBuffer: true };
|
|
1744
|
+
}
|
|
1745
|
+
else if (n === 4) {
|
|
1746
|
+
return {
|
|
1747
|
+
sc: memmem4,
|
|
1748
|
+
needleVal: BigInt(patBytes.readUInt32LE(0)),
|
|
1749
|
+
isWithoutBuffer: true,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
else if (n === 8) {
|
|
1753
|
+
return {
|
|
1754
|
+
sc: memmem8,
|
|
1755
|
+
needleVal: patBytes.readBigUInt64LE(0),
|
|
1756
|
+
isWithoutBuffer: true,
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
return { sc: memmem, needleVal: 0n, isWithoutBuffer: false };
|
|
1760
|
+
}
|
|
1761
|
+
async *scan(address, size, pattern) {
|
|
1762
|
+
const pat = typeof pattern === 'string' ? new Pattern(pattern) : pattern;
|
|
1763
|
+
const startAddr = BigInt(resolveAddress(address));
|
|
1764
|
+
const end = startAddr + BigInt(size);
|
|
1765
|
+
let current = startAddr;
|
|
1766
|
+
while (current < end) {
|
|
1767
|
+
const mbi = await this.root.query(current);
|
|
1768
|
+
const regionBase = BigInt(resolveAddress(mbi.BaseAddress));
|
|
1769
|
+
const regionSize = BigInt(mbi.RegionSize);
|
|
1770
|
+
const regionEnd = regionBase + regionSize;
|
|
1771
|
+
const isReadable = mbi.State === MemoryState.COMMIT &&
|
|
1772
|
+
!(mbi.Protect & MemoryProtection.GUARD) &&
|
|
1773
|
+
!!(mbi.Protect & pat.protect);
|
|
1774
|
+
if (isReadable) {
|
|
1775
|
+
const scanStart = current > regionBase ? current : regionBase;
|
|
1776
|
+
const scanEnd = end < regionEnd ? end : regionEnd;
|
|
1777
|
+
const scanSize = Number(scanEnd - scanStart);
|
|
1778
|
+
if (scanSize >= pat.length) {
|
|
1779
|
+
const n = pat.length;
|
|
1780
|
+
const { sc, needleVal, isWithoutBuffer } = ScannerMiddleware.pickMemmemVariant(n, pat.bytes);
|
|
1781
|
+
const addr = await this.root.machineCode(sc);
|
|
1782
|
+
const memmemFnRemote = sc.cloneForAddress(addr);
|
|
1783
|
+
let needleRemoteAddr = 0n;
|
|
1784
|
+
if (!isWithoutBuffer) {
|
|
1785
|
+
needleRemoteAddr = await this.root.alloc(n);
|
|
1786
|
+
await this.root.write(needleRemoteAddr, pat.bytes);
|
|
1787
|
+
}
|
|
1788
|
+
try {
|
|
1789
|
+
const memmemFn = async (haystack, haystackLen) => {
|
|
1790
|
+
const callArgs = [
|
|
1791
|
+
haystack,
|
|
1792
|
+
haystackLen,
|
|
1793
|
+
isWithoutBuffer ? needleVal : needleRemoteAddr,
|
|
1794
|
+
];
|
|
1795
|
+
if (n !== 1 && n !== 2 && n !== 4 && n !== 8) {
|
|
1796
|
+
callArgs.push(BigInt(n));
|
|
1797
|
+
}
|
|
1798
|
+
const res = await this.root.call(memmemFnRemote, ...callArgs);
|
|
1799
|
+
return BigInt(resolveAddress(res));
|
|
1800
|
+
};
|
|
1801
|
+
yield* Scanner.scan(new NativeMemory(scanStart, scanSize), pat, memmemFn);
|
|
1802
|
+
}
|
|
1803
|
+
finally {
|
|
1804
|
+
if (BigInt(resolveAddress(needleRemoteAddr)) !== 0n) {
|
|
1805
|
+
await this.root.free(needleRemoteAddr, n);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (regionEnd <= current)
|
|
1811
|
+
break;
|
|
1812
|
+
current = regionEnd;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
*scanSync(address, size, pattern) {
|
|
1816
|
+
const pat = typeof pattern === 'string' ? new Pattern(pattern) : pattern;
|
|
1817
|
+
const startAddr = BigInt(resolveAddress(address));
|
|
1818
|
+
const end = startAddr + BigInt(size);
|
|
1819
|
+
let current = startAddr;
|
|
1820
|
+
while (current < end) {
|
|
1821
|
+
const mbi = this.root.querySync(current);
|
|
1822
|
+
const regionBase = BigInt(resolveAddress(mbi.BaseAddress));
|
|
1823
|
+
const regionSize = BigInt(mbi.RegionSize);
|
|
1824
|
+
const regionEnd = regionBase + regionSize;
|
|
1825
|
+
const isReadable = mbi.State === MemoryState.COMMIT &&
|
|
1826
|
+
!(mbi.Protect & MemoryProtection.GUARD) &&
|
|
1827
|
+
!!(mbi.Protect & pat.protect);
|
|
1828
|
+
if (isReadable) {
|
|
1829
|
+
const scanStart = current > regionBase ? current : regionBase;
|
|
1830
|
+
const scanEnd = end < regionEnd ? end : regionEnd;
|
|
1831
|
+
const scanSize = Number(scanEnd - scanStart);
|
|
1832
|
+
if (scanSize >= pat.length) {
|
|
1833
|
+
const n = pat.length;
|
|
1834
|
+
const { sc, needleVal, isWithoutBuffer } = ScannerMiddleware.pickMemmemVariant(n, pat.bytes);
|
|
1835
|
+
const addr = this.root.machineCodeSync(sc);
|
|
1836
|
+
const memmemFnRemote = sc.cloneForAddress(addr);
|
|
1837
|
+
let needleRemoteAddr = 0n;
|
|
1838
|
+
if (!isWithoutBuffer) {
|
|
1839
|
+
needleRemoteAddr = this.root.allocSync(n);
|
|
1840
|
+
this.root.writeSync(needleRemoteAddr, pat.bytes);
|
|
1841
|
+
}
|
|
1842
|
+
try {
|
|
1843
|
+
const memmemFn = (haystack, haystackLen) => {
|
|
1844
|
+
const callArgs = [
|
|
1845
|
+
haystack,
|
|
1846
|
+
haystackLen,
|
|
1847
|
+
isWithoutBuffer ? needleVal : needleRemoteAddr,
|
|
1848
|
+
];
|
|
1849
|
+
if (n !== 1 && n !== 2 && n !== 4 && n !== 8) {
|
|
1850
|
+
callArgs.push(BigInt(n));
|
|
1851
|
+
}
|
|
1852
|
+
const res = this.root.callSync(memmemFnRemote, ...callArgs);
|
|
1853
|
+
return BigInt(resolveAddress(res));
|
|
1854
|
+
};
|
|
1855
|
+
yield* Scanner.scanSync(new NativeMemory(scanStart, scanSize), pat, memmemFn);
|
|
1856
|
+
}
|
|
1857
|
+
finally {
|
|
1858
|
+
if (BigInt(resolveAddress(needleRemoteAddr)) !== 0n) {
|
|
1859
|
+
this.root.freeSync(needleRemoteAddr, n);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (regionEnd <= current)
|
|
1865
|
+
break;
|
|
1866
|
+
current = regionEnd;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
//# sourceMappingURL=middleware-accessor.js.map
|