quickjs-emscripten-sync 1.8.4 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -150
- package/dist/index.d.ts +50 -7
- package/dist/quickjs-emscripten-sync.js +652 -387
- package/dist/quickjs-emscripten-sync.umd.cjs +17 -8
- package/package.json +5 -2
- package/src/index.test.ts +250 -5
- package/src/index.ts +126 -9
- package/src/jsonleak.test.ts +178 -0
- package/src/marshal/custom.ts +16 -2
- package/src/marshal/function.ts +6 -2
- package/src/marshal/hostref.ts +18 -0
- package/src/marshal/index.ts +25 -2
- package/src/marshal/mapset.ts +37 -0
- package/src/marshal/object.ts +4 -1
- package/src/marshal/primitive.ts +2 -11
- package/src/marshal/properties.ts +19 -6
- package/src/unmarshal/custom.ts +35 -4
- package/src/unmarshal/function.ts +2 -2
- package/src/unmarshal/hostref.ts +21 -0
- package/src/unmarshal/index.ts +21 -1
- package/src/unmarshal/mapset.ts +43 -0
- package/src/unmarshal/object.ts +16 -20
- package/src/unmarshal/primitive.ts +6 -17
- package/src/unmarshal/properties.ts +7 -6
- package/src/vmmap.ts +59 -69
- package/src/vmutil.ts +97 -12
- package/src/wrapper.ts +97 -30
package/src/vmutil.ts
CHANGED
|
@@ -3,38 +3,108 @@ import type {
|
|
|
3
3
|
QuickJSContext,
|
|
4
4
|
QuickJSHandle,
|
|
5
5
|
QuickJSDeferredPromise,
|
|
6
|
+
SuccessOrFail,
|
|
6
7
|
} from "quickjs-emscripten";
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Unwrap a VM result, disposing the error handle on failure.
|
|
11
|
+
*
|
|
12
|
+
* `ctx.unwrapResult` throws the error handle without disposing it, which leaks
|
|
13
|
+
* the handle. Under memory pressure the leak cascades: reading the error needs
|
|
14
|
+
* the VM, which is already exhausted, so cleanup is skipped entirely. Here we
|
|
15
|
+
* read the error host-side, always dispose the handle, then throw a host Error.
|
|
16
|
+
*/
|
|
17
|
+
export function unwrapResult<T>(ctx: QuickJSContext, result: SuccessOrFail<T, QuickJSHandle>): T {
|
|
18
|
+
if ("error" in result && result.error) {
|
|
19
|
+
const { error } = result;
|
|
20
|
+
let dumped: any;
|
|
21
|
+
try {
|
|
22
|
+
dumped = ctx.dump(error);
|
|
23
|
+
} catch {
|
|
24
|
+
// VM may be unable to read the error (e.g. out of memory); fall back below.
|
|
25
|
+
} finally {
|
|
26
|
+
if (error.alive) error.dispose();
|
|
27
|
+
}
|
|
28
|
+
const err = new Error(
|
|
29
|
+
typeof dumped === "object" && dumped && "message" in dumped
|
|
30
|
+
? String(dumped.message)
|
|
31
|
+
: dumped !== undefined
|
|
32
|
+
? String(dumped)
|
|
33
|
+
: "quickjs-emscripten-sync: VM evaluation failed",
|
|
34
|
+
);
|
|
35
|
+
if (typeof dumped === "object" && dumped) {
|
|
36
|
+
if ("name" in dumped) err.name = String(dumped.name);
|
|
37
|
+
if ("stack" in dumped) err.stack = String(dumped.stack);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
return result.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
8
44
|
export function fn(
|
|
9
45
|
ctx: QuickJSContext,
|
|
10
46
|
code: string,
|
|
11
47
|
): ((thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]) => QuickJSHandle) & Disposable {
|
|
12
|
-
const handle =
|
|
48
|
+
const handle = unwrapResult(ctx, ctx.evalCode(code));
|
|
13
49
|
const f: any = (thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]): any => {
|
|
14
|
-
return
|
|
50
|
+
return unwrapResult(ctx, ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
|
|
15
51
|
};
|
|
16
52
|
const disposeFn = () => handle.dispose();
|
|
17
53
|
f.dispose = disposeFn;
|
|
18
54
|
f[Symbol.dispose] = disposeFn;
|
|
19
|
-
f.alive = true;
|
|
20
55
|
Object.defineProperty(f, "alive", {
|
|
21
56
|
get: () => handle.alive,
|
|
22
57
|
});
|
|
23
58
|
return f;
|
|
24
59
|
}
|
|
25
60
|
|
|
61
|
+
// Compiled functions for `call` can be cached per context, keyed by code: the
|
|
62
|
+
// code passed to `call` is always a constant literal, so recompiling the same
|
|
63
|
+
// helper on every call (isHandleObject, defineProperties, etc.) was a dominant
|
|
64
|
+
// cost. Caching is opt-in per context via `enableFnCache` because cached
|
|
65
|
+
// handles outlive a single call and must be disposed with `disposeFnCache`;
|
|
66
|
+
// the Arena enables it in its constructor and disposes it in `dispose`. For
|
|
67
|
+
// contexts without a cache, `call` keeps its original compile-and-dispose
|
|
68
|
+
// behaviour so standalone callers don't leak handles.
|
|
69
|
+
const fnCache = new WeakMap<QuickJSContext, Map<string, QuickJSHandle>>();
|
|
70
|
+
|
|
71
|
+
/** Enable per-context caching of compiled functions used by `call`. */
|
|
72
|
+
export function enableFnCache(ctx: QuickJSContext): void {
|
|
73
|
+
if (!fnCache.has(ctx)) fnCache.set(ctx, new Map());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Dispose all compiled functions cached for a context and disable caching. */
|
|
77
|
+
export function disposeFnCache(ctx: QuickJSContext): void {
|
|
78
|
+
const cache = fnCache.get(ctx);
|
|
79
|
+
if (!cache) return;
|
|
80
|
+
for (const handle of cache.values()) {
|
|
81
|
+
if (handle.alive) handle.dispose();
|
|
82
|
+
}
|
|
83
|
+
fnCache.delete(ctx);
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
export function call(
|
|
27
87
|
ctx: QuickJSContext,
|
|
28
88
|
code: string,
|
|
29
89
|
thisArg?: QuickJSHandle,
|
|
30
90
|
...args: QuickJSHandle[]
|
|
31
91
|
): QuickJSHandle {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
92
|
+
const cache = fnCache.get(ctx);
|
|
93
|
+
if (!cache) {
|
|
94
|
+
const f = fn(ctx, code);
|
|
95
|
+
try {
|
|
96
|
+
return f(thisArg, ...args);
|
|
97
|
+
} finally {
|
|
98
|
+
f.dispose();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let handle = cache.get(code);
|
|
103
|
+
if (!handle || !handle.alive) {
|
|
104
|
+
handle = unwrapResult(ctx, ctx.evalCode(code));
|
|
105
|
+
cache.set(code, handle);
|
|
37
106
|
}
|
|
107
|
+
return unwrapResult(ctx, ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
|
|
38
108
|
}
|
|
39
109
|
|
|
40
110
|
export function instanceOf(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHandle): boolean {
|
|
@@ -42,15 +112,30 @@ export function instanceOf(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHand
|
|
|
42
112
|
}
|
|
43
113
|
|
|
44
114
|
export function isHandleObject(ctx: QuickJSContext, h: QuickJSHandle): boolean {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
115
|
+
const type = ctx.typeof(h);
|
|
116
|
+
return type === "function" || (type === "object" && !ctx.sameValue(h, ctx.null));
|
|
48
117
|
}
|
|
49
118
|
|
|
50
119
|
export function json(ctx: QuickJSContext, target: any): QuickJSHandle {
|
|
51
120
|
const json = JSON.stringify(target);
|
|
52
121
|
if (!json) return ctx.undefined;
|
|
53
|
-
|
|
122
|
+
// `call` does not dispose its arguments, so the intermediate string handle
|
|
123
|
+
// must be consumed here or it leaks on every json marshal.
|
|
124
|
+
return ctx.newString(json).consume(s => call(ctx, `JSON.parse`, undefined, s));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run `cb` with `handle`, then dispose `handle` even if `cb` throws.
|
|
129
|
+
*
|
|
130
|
+
* Unlike `handle.consume`, which skips disposal when its callback throws, this
|
|
131
|
+
* helper disposes in a `finally` so error paths don't leak the handle.
|
|
132
|
+
*/
|
|
133
|
+
export function consume<T extends QuickJSHandle, K>(handle: T, cb: (handle: T) => K): K {
|
|
134
|
+
try {
|
|
135
|
+
return cb(handle);
|
|
136
|
+
} finally {
|
|
137
|
+
if (handle.alive) handle.dispose();
|
|
138
|
+
}
|
|
54
139
|
}
|
|
55
140
|
|
|
56
141
|
export function consumeAll<T extends QuickJSHandle[], K>(handles: T, cb: (handles: T) => K): K {
|
package/src/wrapper.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QuickJSHandle, QuickJSContext } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
3
|
import { isObject } from "./util";
|
|
4
|
-
import { call, isHandleObject, mayConsumeAll } from "./vmutil";
|
|
4
|
+
import { call, consume, isHandleObject, mayConsumeAll } from "./vmutil";
|
|
5
5
|
|
|
6
6
|
export type SyncMode = "both" | "vm" | "host";
|
|
7
7
|
|
|
@@ -15,18 +15,28 @@ export function wrap<T = any>(
|
|
|
15
15
|
marshal: (target: any) => [QuickJSHandle, boolean],
|
|
16
16
|
syncMode?: (target: T) => SyncMode | undefined,
|
|
17
17
|
wrappable?: (target: unknown) => boolean,
|
|
18
|
+
syncEnabled = true,
|
|
18
19
|
): Wrapped<T> | undefined {
|
|
19
|
-
//
|
|
20
|
+
// These built-ins rely on internal slots or non-property access, so a proxy
|
|
21
|
+
// would break them; they are marshalled by value instead of being wrapped.
|
|
20
22
|
if (
|
|
21
23
|
!isObject(target) ||
|
|
22
24
|
target instanceof Promise ||
|
|
23
25
|
target instanceof Date ||
|
|
26
|
+
target instanceof ArrayBuffer ||
|
|
27
|
+
ArrayBuffer.isView(target) ||
|
|
28
|
+
target instanceof Map ||
|
|
29
|
+
target instanceof Set ||
|
|
24
30
|
(wrappable && !wrappable(target))
|
|
25
31
|
)
|
|
26
32
|
return undefined;
|
|
27
33
|
|
|
28
34
|
if (isWrapped(target, proxyKeySymbol)) return target;
|
|
29
35
|
|
|
36
|
+
// Sync globally disabled: skip the proxy, but still treat the object as
|
|
37
|
+
// "wrapped" so the rest of the pipeline handles it uniformly.
|
|
38
|
+
if (!syncEnabled) return target as Wrapped<T>;
|
|
39
|
+
|
|
30
40
|
const rec = new Proxy(target as any, {
|
|
31
41
|
get(obj, key) {
|
|
32
42
|
return key === proxyKeySymbol ? obj : Reflect.get(obj, key);
|
|
@@ -34,7 +44,9 @@ export function wrap<T = any>(
|
|
|
34
44
|
set(obj, key, value, receiver) {
|
|
35
45
|
const v = unwrap(value, proxyKeySymbol);
|
|
36
46
|
const sync = syncMode?.(receiver) ?? "host";
|
|
37
|
-
|
|
47
|
+
// Set on the target directly (not via `receiver`) so creating a new
|
|
48
|
+
// property does not re-enter the `defineProperty` trap.
|
|
49
|
+
if ((sync !== "vm" && !Reflect.set(obj, key, v)) || sync === "host" || !ctx.alive)
|
|
38
50
|
return true;
|
|
39
51
|
|
|
40
52
|
mayConsumeAll(
|
|
@@ -68,6 +80,35 @@ export function wrap<T = any>(
|
|
|
68
80
|
return true;
|
|
69
81
|
});
|
|
70
82
|
},
|
|
83
|
+
defineProperty(obj, key, descriptor) {
|
|
84
|
+
const sync = syncMode?.(rec) ?? "host";
|
|
85
|
+
const desc: PropertyDescriptor = { ...descriptor };
|
|
86
|
+
if ("value" in desc) desc.value = unwrap(desc.value, proxyKeySymbol);
|
|
87
|
+
if (typeof desc.get === "function") desc.get = unwrap(desc.get, proxyKeySymbol);
|
|
88
|
+
if (typeof desc.set === "function") desc.set = unwrap(desc.set, proxyKeySymbol);
|
|
89
|
+
|
|
90
|
+
if (sync !== "vm" && !Reflect.defineProperty(obj, key, desc)) return false;
|
|
91
|
+
if (sync === "host" || !ctx.alive) return true;
|
|
92
|
+
|
|
93
|
+
mayConsumeAll(
|
|
94
|
+
[marshal(rec), marshal(key), marshal(desc)],
|
|
95
|
+
(recHandle, keyHandle, descHandle) => {
|
|
96
|
+
const [handle2, unwrapped] = unwrapHandle(ctx, recHandle, proxyKeySymbolHandle);
|
|
97
|
+
const define = (h: QuickJSHandle) =>
|
|
98
|
+
call(
|
|
99
|
+
ctx,
|
|
100
|
+
`(o, k, d) => { Object.defineProperty(o, k, d); }`,
|
|
101
|
+
undefined,
|
|
102
|
+
h,
|
|
103
|
+
keyHandle,
|
|
104
|
+
descHandle,
|
|
105
|
+
).dispose();
|
|
106
|
+
if (unwrapped) handle2.consume(define);
|
|
107
|
+
else define(handle2);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
71
112
|
}) as Wrapped<T>;
|
|
72
113
|
return rec;
|
|
73
114
|
}
|
|
@@ -80,12 +121,16 @@ export function wrapHandle(
|
|
|
80
121
|
unmarshal: (handle: QuickJSHandle) => any,
|
|
81
122
|
syncMode?: (target: QuickJSHandle) => SyncMode | undefined,
|
|
82
123
|
wrappable?: (target: QuickJSHandle, ctx: QuickJSContext) => boolean,
|
|
124
|
+
syncEnabled = true,
|
|
83
125
|
): [Wrapped<QuickJSHandle> | undefined, boolean] {
|
|
84
126
|
if (!isHandleObject(ctx, handle) || (wrappable && !wrappable(handle, ctx)))
|
|
85
127
|
return [undefined, false];
|
|
86
128
|
|
|
87
129
|
if (isHandleWrapped(ctx, handle, proxyKeySymbolHandle)) return [handle, false];
|
|
88
130
|
|
|
131
|
+
// Sync globally disabled: skip the VM-side proxy.
|
|
132
|
+
if (!syncEnabled) return [handle as Wrapped<QuickJSHandle>, false];
|
|
133
|
+
|
|
89
134
|
const getSyncMode = (h: QuickJSHandle) => {
|
|
90
135
|
const res = syncMode?.(unmarshal(h));
|
|
91
136
|
if (typeof res === "string") return ctx.newString(res);
|
|
@@ -108,23 +153,35 @@ export function wrapHandle(
|
|
|
108
153
|
Reflect.deleteProperty(unwrap(target, proxyKeySymbol), key);
|
|
109
154
|
};
|
|
110
155
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
const definer = (h: QuickJSHandle, keyHandle: QuickJSHandle, descHandle: QuickJSHandle) => {
|
|
157
|
+
const target = unmarshal(h);
|
|
158
|
+
if (!target) return;
|
|
159
|
+
const key = unmarshal(keyHandle);
|
|
160
|
+
if (key === "__proto__") return; // for security
|
|
161
|
+
const desc = unmarshal(descHandle);
|
|
162
|
+
Object.defineProperty(unwrap(target, proxyKeySymbol), key, desc);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const proxyFuncs = ctx.newFunction("proxyFuncs", (t, ...args) => {
|
|
166
|
+
const name = ctx.getNumber(t);
|
|
167
|
+
switch (name) {
|
|
168
|
+
case 1:
|
|
169
|
+
return getSyncMode(args[0]);
|
|
170
|
+
case 2:
|
|
171
|
+
return setter(args[0], args[1], args[2]);
|
|
172
|
+
case 3:
|
|
173
|
+
return deleter(args[0], args[1]);
|
|
174
|
+
case 4:
|
|
175
|
+
return definer(args[0], args[1], args[2]);
|
|
176
|
+
}
|
|
177
|
+
return ctx.undefined;
|
|
178
|
+
});
|
|
179
|
+
// Use the exception-safe consume so proxyFuncs is disposed even if compiling
|
|
180
|
+
// the proxy below throws (e.g. under memory pressure).
|
|
181
|
+
return consume(proxyFuncs, proxyFuncs => [
|
|
182
|
+
call(
|
|
183
|
+
ctx,
|
|
184
|
+
`(target, sym, proxyFuncs) => {
|
|
128
185
|
const rec = new Proxy(target, {
|
|
129
186
|
get(obj, key, receiver) {
|
|
130
187
|
return key === sym ? obj : Reflect.get(obj, key, receiver)
|
|
@@ -134,7 +191,7 @@ export function wrapHandle(
|
|
|
134
191
|
? value[sym] ?? value
|
|
135
192
|
: value;
|
|
136
193
|
const sync = proxyFuncs(1, receiver) ?? "vm";
|
|
137
|
-
if (sync === "host" || Reflect.set(obj, key, v
|
|
194
|
+
if (sync === "host" || Reflect.set(obj, key, v)) {
|
|
138
195
|
if (sync !== "vm") {
|
|
139
196
|
proxyFuncs(2, receiver, key, v);
|
|
140
197
|
}
|
|
@@ -150,16 +207,25 @@ export function wrapHandle(
|
|
|
150
207
|
}
|
|
151
208
|
return true;
|
|
152
209
|
},
|
|
210
|
+
defineProperty(obj, key, descriptor) {
|
|
211
|
+
const sync = proxyFuncs(1, rec) ?? "vm";
|
|
212
|
+
if (sync === "host" || Reflect.defineProperty(obj, key, descriptor)) {
|
|
213
|
+
if (sync !== "vm") {
|
|
214
|
+
proxyFuncs(4, rec, key, descriptor);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
153
219
|
});
|
|
154
220
|
return rec;
|
|
155
221
|
}`,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
222
|
+
undefined,
|
|
223
|
+
handle,
|
|
224
|
+
proxyKeySymbolHandle,
|
|
225
|
+
proxyFuncs,
|
|
226
|
+
) as Wrapped<QuickJSHandle>,
|
|
227
|
+
true,
|
|
228
|
+
]);
|
|
163
229
|
}
|
|
164
230
|
|
|
165
231
|
export function unwrap<T>(obj: T, key: string | symbol): T {
|
|
@@ -187,8 +253,9 @@ export function isHandleWrapped(
|
|
|
187
253
|
return !!ctx.dump(
|
|
188
254
|
call(
|
|
189
255
|
ctx,
|
|
190
|
-
//
|
|
191
|
-
|
|
256
|
+
// Built-ins that must not be wrapped (internal slots / non-property access)
|
|
257
|
+
// report as "wrapped" so wrapHandle leaves them alone.
|
|
258
|
+
`(a, s) => (a instanceof Promise) || (a instanceof Date) || (a instanceof ArrayBuffer) || (ArrayBuffer.isView(a)) || (a instanceof Map) || (a instanceof Set) || (typeof a === "object" && a !== null || typeof a === "function") && !!a[s]`,
|
|
192
259
|
undefined,
|
|
193
260
|
handle,
|
|
194
261
|
key,
|