@secure-exec/nodejs 0.2.0-rc.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/LICENSE +191 -0
- package/README.md +7 -0
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +67 -0
- package/dist/bridge/active-handles.d.ts +22 -0
- package/dist/bridge/active-handles.js +112 -0
- package/dist/bridge/child-process.d.ts +99 -0
- package/dist/bridge/child-process.js +672 -0
- package/dist/bridge/dispatch.d.ts +2 -0
- package/dist/bridge/dispatch.js +40 -0
- package/dist/bridge/fs.d.ts +502 -0
- package/dist/bridge/fs.js +3307 -0
- package/dist/bridge/index.d.ts +10 -0
- package/dist/bridge/index.js +41 -0
- package/dist/bridge/module.d.ts +75 -0
- package/dist/bridge/module.js +325 -0
- package/dist/bridge/network.d.ts +1093 -0
- package/dist/bridge/network.js +8651 -0
- package/dist/bridge/os.d.ts +13 -0
- package/dist/bridge/os.js +256 -0
- package/dist/bridge/polyfills.d.ts +9 -0
- package/dist/bridge/polyfills.js +67 -0
- package/dist/bridge/process.d.ts +121 -0
- package/dist/bridge/process.js +1382 -0
- package/dist/bridge/whatwg-url.d.ts +67 -0
- package/dist/bridge/whatwg-url.js +712 -0
- package/dist/bridge-contract.d.ts +774 -0
- package/dist/bridge-contract.js +172 -0
- package/dist/bridge-handlers.d.ts +199 -0
- package/dist/bridge-handlers.js +4263 -0
- package/dist/bridge-loader.d.ts +9 -0
- package/dist/bridge-loader.js +87 -0
- package/dist/bridge-setup.d.ts +1 -0
- package/dist/bridge-setup.js +3 -0
- package/dist/bridge.js +21652 -0
- package/dist/builtin-modules.d.ts +25 -0
- package/dist/builtin-modules.js +312 -0
- package/dist/default-network-adapter.d.ts +13 -0
- package/dist/default-network-adapter.js +351 -0
- package/dist/driver.d.ts +87 -0
- package/dist/driver.js +191 -0
- package/dist/esm-compiler.d.ts +14 -0
- package/dist/esm-compiler.js +68 -0
- package/dist/execution-driver.d.ts +37 -0
- package/dist/execution-driver.js +977 -0
- package/dist/host-network-adapter.d.ts +7 -0
- package/dist/host-network-adapter.js +279 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +23 -0
- package/dist/isolate-bootstrap.d.ts +86 -0
- package/dist/isolate-bootstrap.js +125 -0
- package/dist/ivm-compat.d.ts +7 -0
- package/dist/ivm-compat.js +31 -0
- package/dist/kernel-runtime.d.ts +58 -0
- package/dist/kernel-runtime.js +535 -0
- package/dist/module-access.d.ts +75 -0
- package/dist/module-access.js +606 -0
- package/dist/module-resolver.d.ts +8 -0
- package/dist/module-resolver.js +150 -0
- package/dist/os-filesystem.d.ts +42 -0
- package/dist/os-filesystem.js +161 -0
- package/dist/package-bundler.d.ts +36 -0
- package/dist/package-bundler.js +497 -0
- package/dist/polyfills.d.ts +17 -0
- package/dist/polyfills.js +97 -0
- package/dist/worker-adapter.d.ts +21 -0
- package/dist/worker-adapter.js +34 -0
- package/package.json +123 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { createResolutionCache } from "./package-bundler.js";
|
|
2
|
+
import { getConsoleSetupCode } from "@secure-exec/core/internal/shared/console-formatter";
|
|
3
|
+
import { getRequireSetupCode } from "@secure-exec/core/internal/shared/require-setup";
|
|
4
|
+
import { getIsolateRuntimeSource, getInitialBridgeGlobalsSetupCode } from "@secure-exec/core";
|
|
5
|
+
import { transformDynamicImport } from "@secure-exec/core/internal/shared/esm-utils";
|
|
6
|
+
import { createCommandExecutorStub, createFsStub, createNetworkStub, filterEnv, wrapCommandExecutor, wrapFileSystem, wrapNetworkAdapter, } from "@secure-exec/core/internal/shared/permissions";
|
|
7
|
+
import { createV8Runtime } from "@secure-exec/v8";
|
|
8
|
+
import { getRawBridgeCode, getBridgeAttachCode } from "./bridge-loader.js";
|
|
9
|
+
import { createBudgetState, clearActiveHostTimers, killActiveChildProcesses, normalizePayloadLimit, getExecutionTimeoutMs, getTimingMitigation, PAYLOAD_LIMIT_ERROR_CODE, DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES, DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES, DEFAULT_MAX_TIMERS, DEFAULT_MAX_HANDLES, DEFAULT_SANDBOX_CWD, DEFAULT_SANDBOX_HOME, DEFAULT_SANDBOX_TMPDIR, } from "./isolate-bootstrap.js";
|
|
10
|
+
import { shouldRunAsESM } from "./module-resolver.js";
|
|
11
|
+
import { TIMEOUT_ERROR_MESSAGE, TIMEOUT_EXIT_CODE, ProcessTable, SocketTable, TimerTable, } from "@secure-exec/core";
|
|
12
|
+
import { buildCryptoBridgeHandlers, buildConsoleBridgeHandlers, buildKernelHandleDispatchHandlers, buildKernelTimerDispatchHandlers, buildModuleLoadingBridgeHandlers, buildTimerBridgeHandlers, buildFsBridgeHandlers, buildKernelFdBridgeHandlers, buildChildProcessBridgeHandlers, buildNetworkBridgeHandlers, buildNetworkSocketBridgeHandlers, buildModuleResolutionBridgeHandlers, buildPtyBridgeHandlers, createProcessConfigForExecution, resolveHttpServerResponse, } from "./bridge-handlers.js";
|
|
13
|
+
import { flattenBindingTree, BINDING_PREFIX } from "./bindings.js";
|
|
14
|
+
import { createNodeHostNetworkAdapter } from "./host-network-adapter.js";
|
|
15
|
+
const MAX_ERROR_MESSAGE_CHARS = 8192;
|
|
16
|
+
function boundErrorMessage(message) {
|
|
17
|
+
if (message.length <= MAX_ERROR_MESSAGE_CHARS)
|
|
18
|
+
return message;
|
|
19
|
+
return `${message.slice(0, MAX_ERROR_MESSAGE_CHARS)}...[Truncated]`;
|
|
20
|
+
}
|
|
21
|
+
function createBridgeDriverProcess() {
|
|
22
|
+
return {
|
|
23
|
+
writeStdin() { },
|
|
24
|
+
closeStdin() { },
|
|
25
|
+
kill() { },
|
|
26
|
+
wait: async () => 0,
|
|
27
|
+
onStdout: null,
|
|
28
|
+
onStderr: null,
|
|
29
|
+
onExit: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Shared V8 runtime process — one per Node.js process, lazy-initialized
|
|
33
|
+
let sharedV8Runtime = null;
|
|
34
|
+
let sharedV8RuntimePromise = null;
|
|
35
|
+
async function getSharedV8Runtime() {
|
|
36
|
+
if (sharedV8Runtime?.isAlive)
|
|
37
|
+
return sharedV8Runtime;
|
|
38
|
+
if (sharedV8RuntimePromise)
|
|
39
|
+
return sharedV8RuntimePromise;
|
|
40
|
+
// Build bridge code for snapshot warmup
|
|
41
|
+
const bridgeCode = buildFullBridgeCode();
|
|
42
|
+
sharedV8RuntimePromise = createV8Runtime({
|
|
43
|
+
warmupBridgeCode: bridgeCode,
|
|
44
|
+
}).then((rt) => {
|
|
45
|
+
sharedV8Runtime = rt;
|
|
46
|
+
sharedV8RuntimePromise = null;
|
|
47
|
+
return rt;
|
|
48
|
+
});
|
|
49
|
+
return sharedV8RuntimePromise;
|
|
50
|
+
}
|
|
51
|
+
// Minimal polyfills for APIs the bridge IIFE expects but the Rust V8 runtime doesn't provide.
|
|
52
|
+
const V8_POLYFILLS = `
|
|
53
|
+
if (typeof SharedArrayBuffer === 'undefined') {
|
|
54
|
+
globalThis.SharedArrayBuffer = class SharedArrayBuffer extends ArrayBuffer {};
|
|
55
|
+
var _abBL = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength');
|
|
56
|
+
if (_abBL) Object.defineProperty(SharedArrayBuffer.prototype, 'byteLength', _abBL);
|
|
57
|
+
Object.defineProperty(SharedArrayBuffer.prototype, 'growable', { get() { return false; } });
|
|
58
|
+
}
|
|
59
|
+
if (!Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'resizable')) {
|
|
60
|
+
Object.defineProperty(ArrayBuffer.prototype, 'resizable', { get() { return false; } });
|
|
61
|
+
}
|
|
62
|
+
if (typeof queueMicrotask === 'undefined') globalThis.queueMicrotask = (fn) => Promise.resolve().then(fn);
|
|
63
|
+
if (typeof atob === 'undefined') {
|
|
64
|
+
globalThis.atob = (s) => {
|
|
65
|
+
const b = typeof Buffer !== 'undefined' ? Buffer : null;
|
|
66
|
+
if (b) return b.from(s, 'base64').toString('binary');
|
|
67
|
+
// Fallback: manual base64 decode
|
|
68
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
69
|
+
let out = ''; for (let i = 0; i < s.length;) {
|
|
70
|
+
const a = chars.indexOf(s[i++]), b2 = chars.indexOf(s[i++]), c = chars.indexOf(s[i++]), d = chars.indexOf(s[i++]);
|
|
71
|
+
out += String.fromCharCode((a<<2)|(b2>>4)); if (c!==64) out += String.fromCharCode(((b2&15)<<4)|(c>>2)); if (d!==64) out += String.fromCharCode(((c&3)<<6)|d);
|
|
72
|
+
} return out;
|
|
73
|
+
};
|
|
74
|
+
globalThis.btoa = (s) => {
|
|
75
|
+
const b = typeof Buffer !== 'undefined' ? Buffer : null;
|
|
76
|
+
if (b) return b.from(s, 'binary').toString('base64');
|
|
77
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
78
|
+
let out = ''; for (let i = 0; i < s.length;) {
|
|
79
|
+
const a = s.charCodeAt(i++), b2 = s.charCodeAt(i++), c = s.charCodeAt(i++);
|
|
80
|
+
out += chars[a>>2] + chars[((a&3)<<4)|(b2>>4)] + (isNaN(b2) ? '=' : chars[((b2&15)<<2)|(c>>4)]) + (isNaN(c) ? '=' : chars[c&63]);
|
|
81
|
+
} return out;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (typeof TextEncoder === 'undefined') {
|
|
85
|
+
const _encodeUtf8 = (str = '') => {
|
|
86
|
+
const bytes = [];
|
|
87
|
+
for (let i = 0; i < str.length; i++) {
|
|
88
|
+
const codeUnit = str.charCodeAt(i);
|
|
89
|
+
let codePoint = codeUnit;
|
|
90
|
+
if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF) {
|
|
91
|
+
const next = i + 1 < str.length ? str.charCodeAt(i + 1) : 0;
|
|
92
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
93
|
+
codePoint = 0x10000 + ((codeUnit - 0xD800) << 10) + (next - 0xDC00);
|
|
94
|
+
i++;
|
|
95
|
+
} else {
|
|
96
|
+
codePoint = 0xFFFD;
|
|
97
|
+
}
|
|
98
|
+
} else if (codeUnit >= 0xDC00 && codeUnit <= 0xDFFF) {
|
|
99
|
+
codePoint = 0xFFFD;
|
|
100
|
+
}
|
|
101
|
+
if (codePoint < 0x80) bytes.push(codePoint);
|
|
102
|
+
else if (codePoint < 0x800) bytes.push(0xC0 | (codePoint >> 6), 0x80 | (codePoint & 63));
|
|
103
|
+
else if (codePoint < 0x10000) bytes.push(0xE0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 63), 0x80 | (codePoint & 63));
|
|
104
|
+
else bytes.push(0xF0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 63), 0x80 | ((codePoint >> 6) & 63), 0x80 | (codePoint & 63));
|
|
105
|
+
}
|
|
106
|
+
return new Uint8Array(bytes);
|
|
107
|
+
};
|
|
108
|
+
globalThis.TextEncoder = class TextEncoder {
|
|
109
|
+
encode(str = '') { return _encodeUtf8(String(str)); }
|
|
110
|
+
get encoding() { return 'utf-8'; }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (typeof TextDecoder === 'undefined') {
|
|
114
|
+
globalThis.TextDecoder = class TextDecoder {
|
|
115
|
+
constructor() {}
|
|
116
|
+
decode(buf) { if (!buf) return ''; const u8 = new Uint8Array(buf.buffer || buf); let s = ''; for (let i = 0; i < u8.length;) { const b = u8[i++]; if (b < 128) s += String.fromCharCode(b); else if (b < 224) s += String.fromCharCode(((b&31)<<6)|(u8[i++]&63)); else if (b < 240) { const b2 = u8[i++]; s += String.fromCharCode(((b&15)<<12)|((b2&63)<<6)|(u8[i++]&63)); } else { const b2 = u8[i++], b3 = u8[i++], cp = ((b&7)<<18)|((b2&63)<<12)|((b3&63)<<6)|(u8[i++]&63); if (cp>0xFFFF) { const s2 = cp-0x10000; s += String.fromCharCode(0xD800+(s2>>10), 0xDC00+(s2&0x3FF)); } else s += String.fromCharCode(cp); } } return s; }
|
|
117
|
+
get encoding() { return 'utf-8'; }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (typeof URL === 'undefined') {
|
|
121
|
+
globalThis.URL = class URL {
|
|
122
|
+
constructor(url, base) { const m = String(base ? new URL(base).href : ''); const full = url.startsWith('http') ? url : m.replace(/\\/[^\\/]*$/, '/') + url; const pm = full.match(/^(\\w+:)\\/\\/([^/:]+)(:\\d+)?(.*)$/); this.protocol = pm?.[1]||''; this.hostname = pm?.[2]||''; this.port = (pm?.[3]||'').slice(1); this.pathname = (pm?.[4]||'/').split('?')[0].split('#')[0]; this.search = full.includes('?') ? '?'+full.split('?')[1].split('#')[0] : ''; this.hash = full.includes('#') ? '#'+full.split('#')[1] : ''; this.host = this.hostname + (this.port ? ':'+this.port : ''); this.href = this.protocol+'//'+this.host+this.pathname+this.search+this.hash; this.origin = this.protocol+'//'+this.host; this.searchParams = typeof URLSearchParams !== 'undefined' ? new URLSearchParams(this.search) : { get:()=>null }; }
|
|
123
|
+
toString() { return this.href; }
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (typeof URLSearchParams === 'undefined') {
|
|
127
|
+
globalThis.URLSearchParams = class URLSearchParams {
|
|
128
|
+
constructor(init) { this._map = new Map(); if (typeof init === 'string') { for (const p of init.replace(/^\\?/,'').split('&')) { const [k,...v] = p.split('='); if (k) this._map.set(decodeURIComponent(k), decodeURIComponent(v.join('='))); } } }
|
|
129
|
+
get(k) { return this._map.get(k) ?? null; }
|
|
130
|
+
has(k) { return this._map.has(k); }
|
|
131
|
+
toString() { return [...this._map].map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'); }
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (typeof structuredClone === 'undefined') {
|
|
135
|
+
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
|
|
136
|
+
}
|
|
137
|
+
if (typeof performance === 'undefined') {
|
|
138
|
+
globalThis.performance = { now: () => Date.now(), timeOrigin: Date.now() };
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
typeof AbortController === 'undefined' ||
|
|
142
|
+
typeof AbortSignal === 'undefined' ||
|
|
143
|
+
typeof AbortSignal.prototype?.addEventListener !== 'function' ||
|
|
144
|
+
typeof AbortSignal.prototype?.removeEventListener !== 'function'
|
|
145
|
+
) {
|
|
146
|
+
const abortSignalState = new WeakMap();
|
|
147
|
+
function getAbortSignalState(signal) {
|
|
148
|
+
const state = abortSignalState.get(signal);
|
|
149
|
+
if (!state) throw new Error('Invalid AbortSignal');
|
|
150
|
+
return state;
|
|
151
|
+
}
|
|
152
|
+
class AbortSignal {
|
|
153
|
+
constructor() {
|
|
154
|
+
this.onabort = null;
|
|
155
|
+
abortSignalState.set(this, {
|
|
156
|
+
aborted: false,
|
|
157
|
+
reason: undefined,
|
|
158
|
+
listeners: [],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
get aborted() {
|
|
162
|
+
return getAbortSignalState(this).aborted;
|
|
163
|
+
}
|
|
164
|
+
get reason() {
|
|
165
|
+
return getAbortSignalState(this).reason;
|
|
166
|
+
}
|
|
167
|
+
get _listeners() {
|
|
168
|
+
return getAbortSignalState(this).listeners.slice();
|
|
169
|
+
}
|
|
170
|
+
getEventListeners(type) {
|
|
171
|
+
if (type !== 'abort') return [];
|
|
172
|
+
return getAbortSignalState(this).listeners.slice();
|
|
173
|
+
}
|
|
174
|
+
addEventListener(type, listener) {
|
|
175
|
+
if (type !== 'abort' || typeof listener !== 'function') return;
|
|
176
|
+
getAbortSignalState(this).listeners.push(listener);
|
|
177
|
+
}
|
|
178
|
+
removeEventListener(type, listener) {
|
|
179
|
+
if (type !== 'abort' || typeof listener !== 'function') return;
|
|
180
|
+
const listeners = getAbortSignalState(this).listeners;
|
|
181
|
+
const index = listeners.indexOf(listener);
|
|
182
|
+
if (index !== -1) {
|
|
183
|
+
listeners.splice(index, 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
dispatchEvent(event) {
|
|
187
|
+
if (!event || event.type !== 'abort') return false;
|
|
188
|
+
if (typeof this.onabort === 'function') {
|
|
189
|
+
try {
|
|
190
|
+
this.onabort.call(this, event);
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
const listeners = getAbortSignalState(this).listeners.slice();
|
|
194
|
+
for (const listener of listeners) {
|
|
195
|
+
try {
|
|
196
|
+
listener.call(this, event);
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
globalThis.AbortSignal = AbortSignal;
|
|
203
|
+
globalThis.AbortController = class AbortController {
|
|
204
|
+
constructor() {
|
|
205
|
+
this.signal = new AbortSignal();
|
|
206
|
+
}
|
|
207
|
+
abort(reason) {
|
|
208
|
+
const state = getAbortSignalState(this.signal);
|
|
209
|
+
if (state.aborted) return;
|
|
210
|
+
state.aborted = true;
|
|
211
|
+
state.reason = reason;
|
|
212
|
+
this.signal.dispatchEvent({ type: 'abort' });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (
|
|
217
|
+
typeof globalThis.AbortSignal === 'function' &&
|
|
218
|
+
typeof globalThis.AbortController === 'function' &&
|
|
219
|
+
typeof globalThis.AbortSignal.abort !== 'function'
|
|
220
|
+
) {
|
|
221
|
+
globalThis.AbortSignal.abort = function abort(reason) {
|
|
222
|
+
const controller = new globalThis.AbortController();
|
|
223
|
+
controller.abort(reason);
|
|
224
|
+
return controller.signal;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (typeof navigator === 'undefined') {
|
|
228
|
+
globalThis.navigator = { userAgent: 'secure-exec-v8' };
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
// Shim for ivm.Reference methods used by bridge code.
|
|
232
|
+
// Bridge globals in the V8 runtime are plain functions, but the bridge code
|
|
233
|
+
// (compiled from @secure-exec/core) calls them via .applySync(), .apply(), and
|
|
234
|
+
// .applySyncPromise() which are ivm Reference calling patterns.
|
|
235
|
+
// Shim for native bridge functions (runs early in postRestoreScript)
|
|
236
|
+
const BRIDGE_NATIVE_SHIM = `
|
|
237
|
+
(function() {
|
|
238
|
+
var _origApply = Function.prototype.apply;
|
|
239
|
+
function shimBridgeGlobal(name) {
|
|
240
|
+
var fn = globalThis[name];
|
|
241
|
+
if (typeof fn !== 'function' || fn.applySync) return;
|
|
242
|
+
fn.applySync = function(_, args) { return _origApply.call(fn, null, args || []); };
|
|
243
|
+
fn.applySyncPromise = function(_, args) { return _origApply.call(fn, null, args || []); };
|
|
244
|
+
fn.derefInto = function() { return fn; };
|
|
245
|
+
}
|
|
246
|
+
var keys = Object.getOwnPropertyNames(globalThis).filter(function(k) { return k.startsWith('_') && typeof globalThis[k] === 'function'; });
|
|
247
|
+
keys.forEach(shimBridgeGlobal);
|
|
248
|
+
})();
|
|
249
|
+
`;
|
|
250
|
+
// Dispatch shim for bridge globals not natively supported by the V8 binary.
|
|
251
|
+
// Installs dispatch wrappers for ALL known bridge globals that aren't already
|
|
252
|
+
// functions. This runs BEFORE require-setup so the crypto/net module code
|
|
253
|
+
// detects the dispatch-wrapped globals and installs the corresponding APIs.
|
|
254
|
+
function buildBridgeDispatchShim() {
|
|
255
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
256
|
+
// Collect all bridge global names from the contract
|
|
257
|
+
const allGlobals = Object.values(K).filter(v => typeof v === "string");
|
|
258
|
+
return `
|
|
259
|
+
(function() {
|
|
260
|
+
var _origApply = Function.prototype.apply;
|
|
261
|
+
function encodeDispatchArgs(args) {
|
|
262
|
+
return JSON.stringify(args, function(_key, value) {
|
|
263
|
+
if (value === undefined) {
|
|
264
|
+
return { __secureExecDispatchType: 'undefined' };
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
var names = ${JSON.stringify(allGlobals)};
|
|
270
|
+
for (var i = 0; i < names.length; i++) {
|
|
271
|
+
var name = names[i];
|
|
272
|
+
if (typeof globalThis[name] === 'function') continue;
|
|
273
|
+
(function(n) {
|
|
274
|
+
function reviveDispatchError(payload) {
|
|
275
|
+
var error = new Error(payload && payload.message ? payload.message : String(payload));
|
|
276
|
+
if (payload && payload.name) error.name = payload.name;
|
|
277
|
+
if (payload && payload.code !== undefined) error.code = payload.code;
|
|
278
|
+
if (payload && payload.stack) error.stack = payload.stack;
|
|
279
|
+
return error;
|
|
280
|
+
}
|
|
281
|
+
var fn = function() {
|
|
282
|
+
var args = Array.prototype.slice.call(arguments);
|
|
283
|
+
var encoded = "__bd:" + n + ":" + encodeDispatchArgs(args);
|
|
284
|
+
var resultJson = _loadPolyfill.applySyncPromise(undefined, [encoded]);
|
|
285
|
+
if (resultJson === null) return undefined;
|
|
286
|
+
try {
|
|
287
|
+
var parsed = JSON.parse(resultJson);
|
|
288
|
+
if (parsed.__bd_error) throw reviveDispatchError(parsed.__bd_error);
|
|
289
|
+
return parsed.__bd_result;
|
|
290
|
+
} catch (e) {
|
|
291
|
+
if (e.message && e.message.startsWith('No handler:')) return undefined;
|
|
292
|
+
throw e;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
fn.applySync = function(_, args) { return _origApply.call(fn, null, args || []); };
|
|
296
|
+
fn.applySyncPromise = function(_, args) { return _origApply.call(fn, null, args || []); };
|
|
297
|
+
fn.derefInto = function() { return fn; };
|
|
298
|
+
globalThis[n] = fn;
|
|
299
|
+
})(name);
|
|
300
|
+
}
|
|
301
|
+
})();
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
const BRIDGE_DISPATCH_SHIM = buildBridgeDispatchShim();
|
|
305
|
+
// Cache assembled bridge code (same across all executions)
|
|
306
|
+
let bridgeCodeCache = null;
|
|
307
|
+
function buildFullBridgeCode() {
|
|
308
|
+
if (bridgeCodeCache)
|
|
309
|
+
return bridgeCodeCache;
|
|
310
|
+
// Assemble the full bridge code IIFE from component scripts.
|
|
311
|
+
// Only include code that can run without bridge calls (snapshot phase).
|
|
312
|
+
// Console/require/fsFacade setup goes in postRestoreScript where bridge calls work.
|
|
313
|
+
const parts = [
|
|
314
|
+
// Polyfill missing Web APIs for the Rust V8 runtime
|
|
315
|
+
V8_POLYFILLS,
|
|
316
|
+
getIsolateRuntimeSource("globalExposureHelpers"),
|
|
317
|
+
getInitialBridgeGlobalsSetupCode(),
|
|
318
|
+
getRawBridgeCode(),
|
|
319
|
+
getBridgeAttachCode(),
|
|
320
|
+
];
|
|
321
|
+
bridgeCodeCache = parts.join("\n");
|
|
322
|
+
return bridgeCodeCache;
|
|
323
|
+
}
|
|
324
|
+
export class NodeExecutionDriver {
|
|
325
|
+
state;
|
|
326
|
+
memoryLimit;
|
|
327
|
+
disposed = false;
|
|
328
|
+
flattenedBindings = null;
|
|
329
|
+
// Unwrapped filesystem for path translation (toHostPath/toSandboxPath)
|
|
330
|
+
rawFilesystem;
|
|
331
|
+
// Kernel socket table for routing net.connect through kernel
|
|
332
|
+
socketTable;
|
|
333
|
+
// Kernel process table for child process registration
|
|
334
|
+
processTable;
|
|
335
|
+
timerTable;
|
|
336
|
+
ownsProcessTable;
|
|
337
|
+
ownsTimerTable;
|
|
338
|
+
configuredMaxTimers;
|
|
339
|
+
configuredMaxHandles;
|
|
340
|
+
pid;
|
|
341
|
+
constructor(options) {
|
|
342
|
+
this.memoryLimit = options.memoryLimit ?? 128;
|
|
343
|
+
const budgets = options.resourceBudgets;
|
|
344
|
+
this.socketTable = options.socketTable;
|
|
345
|
+
this.processTable = options.processTable ?? new ProcessTable();
|
|
346
|
+
this.timerTable = options.timerTable ?? new TimerTable();
|
|
347
|
+
this.ownsProcessTable = options.processTable === undefined;
|
|
348
|
+
this.ownsTimerTable = options.timerTable === undefined;
|
|
349
|
+
this.configuredMaxTimers = budgets?.maxTimers;
|
|
350
|
+
this.configuredMaxHandles = budgets?.maxHandles;
|
|
351
|
+
this.pid = options.pid ?? 1;
|
|
352
|
+
const system = options.system;
|
|
353
|
+
const permissions = system.permissions;
|
|
354
|
+
if (!this.socketTable) {
|
|
355
|
+
this.socketTable = new SocketTable({
|
|
356
|
+
hostAdapter: createNodeHostNetworkAdapter(),
|
|
357
|
+
networkCheck: permissions?.network,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// Keep unwrapped filesystem for path translation (toHostPath/toSandboxPath)
|
|
361
|
+
this.rawFilesystem = system.filesystem;
|
|
362
|
+
const filesystem = this.rawFilesystem
|
|
363
|
+
? wrapFileSystem(this.rawFilesystem, permissions)
|
|
364
|
+
: createFsStub();
|
|
365
|
+
const commandExecutor = system.commandExecutor
|
|
366
|
+
? wrapCommandExecutor(system.commandExecutor, permissions)
|
|
367
|
+
: createCommandExecutorStub();
|
|
368
|
+
const rawNetworkAdapter = system.network;
|
|
369
|
+
const networkAdapter = rawNetworkAdapter
|
|
370
|
+
? wrapNetworkAdapter(rawNetworkAdapter, permissions)
|
|
371
|
+
: createNetworkStub();
|
|
372
|
+
const loopbackAwareAdapter = networkAdapter;
|
|
373
|
+
if (loopbackAwareAdapter.__setLoopbackPortChecker && this.socketTable) {
|
|
374
|
+
loopbackAwareAdapter.__setLoopbackPortChecker((_hostname, port) => this.socketTable?.findListener({ host: "127.0.0.1", port }) !== null);
|
|
375
|
+
}
|
|
376
|
+
const processConfig = { ...(options.runtime.process ?? {}) };
|
|
377
|
+
processConfig.cwd ??= DEFAULT_SANDBOX_CWD;
|
|
378
|
+
processConfig.env = filterEnv(processConfig.env, permissions);
|
|
379
|
+
const osConfig = { ...(options.runtime.os ?? {}) };
|
|
380
|
+
osConfig.homedir ??= DEFAULT_SANDBOX_HOME;
|
|
381
|
+
osConfig.tmpdir ??= DEFAULT_SANDBOX_TMPDIR;
|
|
382
|
+
const bridgeBase64TransferLimitBytes = normalizePayloadLimit(options.payloadLimits?.base64TransferBytes, DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES, "payloadLimits.base64TransferBytes");
|
|
383
|
+
const isolateJsonPayloadLimitBytes = normalizePayloadLimit(options.payloadLimits?.jsonPayloadBytes, DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES, "payloadLimits.jsonPayloadBytes");
|
|
384
|
+
this.state = {
|
|
385
|
+
filesystem,
|
|
386
|
+
commandExecutor,
|
|
387
|
+
networkAdapter,
|
|
388
|
+
permissions,
|
|
389
|
+
processConfig,
|
|
390
|
+
osConfig,
|
|
391
|
+
onStdio: options.onStdio,
|
|
392
|
+
cpuTimeLimitMs: options.cpuTimeLimitMs,
|
|
393
|
+
timingMitigation: options.timingMitigation ?? "freeze",
|
|
394
|
+
bridgeBase64TransferLimitBytes,
|
|
395
|
+
isolateJsonPayloadLimitBytes,
|
|
396
|
+
maxOutputBytes: budgets?.maxOutputBytes,
|
|
397
|
+
maxBridgeCalls: budgets?.maxBridgeCalls,
|
|
398
|
+
maxChildProcesses: budgets?.maxChildProcesses,
|
|
399
|
+
maxTimers: budgets?.maxTimers,
|
|
400
|
+
maxHandles: budgets?.maxHandles,
|
|
401
|
+
budgetState: createBudgetState(),
|
|
402
|
+
activeHttpServerIds: new Set(),
|
|
403
|
+
activeHttpServerClosers: new Map(),
|
|
404
|
+
pendingHttpServerStarts: { count: 0 },
|
|
405
|
+
activeChildProcesses: new Map(),
|
|
406
|
+
activeHostTimers: new Set(),
|
|
407
|
+
moduleFormatCache: new Map(),
|
|
408
|
+
packageTypeCache: new Map(),
|
|
409
|
+
resolutionCache: createResolutionCache(),
|
|
410
|
+
onPtySetRawMode: options.onPtySetRawMode,
|
|
411
|
+
};
|
|
412
|
+
// Validate and flatten bindings once at construction time
|
|
413
|
+
if (options.bindings) {
|
|
414
|
+
this.flattenedBindings = flattenBindingTree(options.bindings);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
get network() {
|
|
418
|
+
const adapter = this.state.networkAdapter ?? createNetworkStub();
|
|
419
|
+
return {
|
|
420
|
+
fetch: (url, options) => adapter.fetch(url, options),
|
|
421
|
+
dnsLookup: (hostname) => adapter.dnsLookup(hostname),
|
|
422
|
+
httpRequest: (url, options) => adapter.httpRequest(url, options),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
get unsafeIsolate() { return null; }
|
|
426
|
+
hasManagedResources() {
|
|
427
|
+
return (this.state.pendingHttpServerStarts.count > 0 ||
|
|
428
|
+
this.state.activeHttpServerIds.size > 0 ||
|
|
429
|
+
this.state.activeChildProcesses.size > 0 ||
|
|
430
|
+
(!this.ownsProcessTable && this.state.activeHostTimers.size > 0));
|
|
431
|
+
}
|
|
432
|
+
async waitForManagedResources() {
|
|
433
|
+
const graceDeadline = Date.now() + 100;
|
|
434
|
+
// Give async bridge callbacks a moment to register their host-side handles.
|
|
435
|
+
while (!this.disposed && !this.hasManagedResources() && Date.now() < graceDeadline) {
|
|
436
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
437
|
+
}
|
|
438
|
+
// Keep the session alive while host-managed resources are still active.
|
|
439
|
+
while (!this.disposed && this.hasManagedResources()) {
|
|
440
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
ensureBridgeProcessEntry(processConfig) {
|
|
444
|
+
if (this.pid === undefined || !this.processTable)
|
|
445
|
+
return;
|
|
446
|
+
const entry = this.processTable.get(this.pid);
|
|
447
|
+
if (!entry || entry.status === "exited") {
|
|
448
|
+
this.processTable.register(this.pid, "node", "node", [], {
|
|
449
|
+
pid: this.pid,
|
|
450
|
+
ppid: 0,
|
|
451
|
+
env: processConfig.env ?? {},
|
|
452
|
+
cwd: processConfig.cwd ?? DEFAULT_SANDBOX_CWD,
|
|
453
|
+
fds: { stdin: 0, stdout: 1, stderr: 2 },
|
|
454
|
+
stdinIsTTY: processConfig.stdinIsTTY,
|
|
455
|
+
stdoutIsTTY: processConfig.stdoutIsTTY,
|
|
456
|
+
stderrIsTTY: processConfig.stderrIsTTY,
|
|
457
|
+
}, createBridgeDriverProcess());
|
|
458
|
+
}
|
|
459
|
+
if (this.ownsProcessTable || this.configuredMaxHandles !== undefined) {
|
|
460
|
+
this.processTable.setHandleLimit(this.pid, this.configuredMaxHandles ?? DEFAULT_MAX_HANDLES);
|
|
461
|
+
}
|
|
462
|
+
if (this.ownsTimerTable || this.configuredMaxTimers !== undefined) {
|
|
463
|
+
this.timerTable.setLimit(this.pid, this.configuredMaxTimers ?? DEFAULT_MAX_TIMERS);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
clearKernelTimersForProcess(pid) {
|
|
467
|
+
for (const timer of this.timerTable.getActiveTimers(pid)) {
|
|
468
|
+
if (timer.hostHandle !== undefined) {
|
|
469
|
+
clearTimeout(timer.hostHandle);
|
|
470
|
+
this.state.activeHostTimers.delete(timer.hostHandle);
|
|
471
|
+
timer.hostHandle = undefined;
|
|
472
|
+
}
|
|
473
|
+
this.timerTable.clearTimer(timer.id);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
finalizeExecutionState(exitCode) {
|
|
477
|
+
if (this.pid === undefined)
|
|
478
|
+
return;
|
|
479
|
+
this.clearKernelTimersForProcess(this.pid);
|
|
480
|
+
if (this.ownsProcessTable && this.processTable) {
|
|
481
|
+
this.processTable.markExited(this.pid, exitCode);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async createUnsafeContext(_options = {}) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
async run(code, filePath) {
|
|
488
|
+
return this.executeInternal({ mode: "run", code, filePath });
|
|
489
|
+
}
|
|
490
|
+
async exec(code, options) {
|
|
491
|
+
const result = await this.executeInternal({
|
|
492
|
+
mode: "exec",
|
|
493
|
+
code,
|
|
494
|
+
filePath: options?.filePath,
|
|
495
|
+
env: options?.env,
|
|
496
|
+
cwd: options?.cwd,
|
|
497
|
+
stdin: options?.stdin,
|
|
498
|
+
cpuTimeLimitMs: options?.cpuTimeLimitMs,
|
|
499
|
+
timingMitigation: options?.timingMitigation,
|
|
500
|
+
onStdio: options?.onStdio,
|
|
501
|
+
});
|
|
502
|
+
return { code: result.code, errorMessage: result.errorMessage };
|
|
503
|
+
}
|
|
504
|
+
async executeInternal(options) {
|
|
505
|
+
if (this.disposed)
|
|
506
|
+
throw new Error("NodeExecutionDriver has been disposed");
|
|
507
|
+
// Reset per-execution state
|
|
508
|
+
this.state.budgetState = createBudgetState();
|
|
509
|
+
this.state.moduleFormatCache.clear();
|
|
510
|
+
this.state.packageTypeCache.clear();
|
|
511
|
+
this.state.resolutionCache.resolveResults.clear();
|
|
512
|
+
this.state.resolutionCache.packageJsonResults.clear();
|
|
513
|
+
this.state.resolutionCache.existsResults.clear();
|
|
514
|
+
this.state.resolutionCache.statResults.clear();
|
|
515
|
+
const s = this.state;
|
|
516
|
+
const timingMitigation = getTimingMitigation(options.timingMitigation, s.timingMitigation);
|
|
517
|
+
const frozenTimeMs = Date.now();
|
|
518
|
+
const onStdio = options.onStdio ?? s.onStdio;
|
|
519
|
+
const entryIsEsm = await shouldRunAsESM({
|
|
520
|
+
filesystem: s.filesystem,
|
|
521
|
+
packageTypeCache: s.packageTypeCache,
|
|
522
|
+
moduleFormatCache: s.moduleFormatCache,
|
|
523
|
+
isolateJsonPayloadLimitBytes: s.isolateJsonPayloadLimitBytes,
|
|
524
|
+
resolutionCache: s.resolutionCache,
|
|
525
|
+
}, options.code, options.filePath);
|
|
526
|
+
const sessionMode = options.mode === "run" || entryIsEsm ? "run" : "exec";
|
|
527
|
+
const userCode = entryIsEsm
|
|
528
|
+
? options.code
|
|
529
|
+
: transformDynamicImport(options.code);
|
|
530
|
+
// Get or create V8 runtime
|
|
531
|
+
const v8Runtime = await getSharedV8Runtime();
|
|
532
|
+
const cpuTimeLimitMs = getExecutionTimeoutMs(options.cpuTimeLimitMs, s.cpuTimeLimitMs);
|
|
533
|
+
const sessionOpts = {
|
|
534
|
+
heapLimitMb: this.memoryLimit,
|
|
535
|
+
cpuTimeLimitMs,
|
|
536
|
+
};
|
|
537
|
+
const session = await v8Runtime.createSession(sessionOpts);
|
|
538
|
+
let finalExitCode = 0;
|
|
539
|
+
try {
|
|
540
|
+
const execProcessConfig = createProcessConfigForExecution(options.env || options.cwd
|
|
541
|
+
? {
|
|
542
|
+
...s.processConfig,
|
|
543
|
+
...(options.env ? { env: filterEnv(options.env, s.permissions) } : {}),
|
|
544
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
545
|
+
}
|
|
546
|
+
: s.processConfig, timingMitigation, frozenTimeMs);
|
|
547
|
+
this.ensureBridgeProcessEntry(execProcessConfig);
|
|
548
|
+
// Build bridge handlers for this execution
|
|
549
|
+
const cryptoResult = buildCryptoBridgeHandlers();
|
|
550
|
+
const sendStreamEvent = (eventType, payload) => {
|
|
551
|
+
try {
|
|
552
|
+
session.sendStreamEvent(eventType, payload);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// Session may be destroyed
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const netSocketResult = buildNetworkSocketBridgeHandlers({
|
|
559
|
+
dispatch: (socketId, event, data) => {
|
|
560
|
+
const payload = JSON.stringify({ socketId, event, data });
|
|
561
|
+
sendStreamEvent("netSocket", Buffer.from(payload));
|
|
562
|
+
},
|
|
563
|
+
socketTable: this.socketTable,
|
|
564
|
+
pid: this.pid,
|
|
565
|
+
});
|
|
566
|
+
const networkBridgeResult = buildNetworkBridgeHandlers({
|
|
567
|
+
networkAdapter: s.networkAdapter,
|
|
568
|
+
budgetState: s.budgetState,
|
|
569
|
+
maxBridgeCalls: s.maxBridgeCalls,
|
|
570
|
+
isolateJsonPayloadLimitBytes: s.isolateJsonPayloadLimitBytes,
|
|
571
|
+
activeHttpServerIds: s.activeHttpServerIds,
|
|
572
|
+
activeHttpServerClosers: s.activeHttpServerClosers,
|
|
573
|
+
pendingHttpServerStarts: s.pendingHttpServerStarts,
|
|
574
|
+
sendStreamEvent,
|
|
575
|
+
socketTable: this.socketTable,
|
|
576
|
+
pid: this.pid,
|
|
577
|
+
});
|
|
578
|
+
const kernelFdResult = buildKernelFdBridgeHandlers({
|
|
579
|
+
filesystem: s.filesystem,
|
|
580
|
+
budgetState: s.budgetState,
|
|
581
|
+
maxBridgeCalls: s.maxBridgeCalls,
|
|
582
|
+
});
|
|
583
|
+
const kernelTimerDispatchHandlers = buildKernelTimerDispatchHandlers({
|
|
584
|
+
timerTable: this.timerTable,
|
|
585
|
+
pid: this.pid ?? 1,
|
|
586
|
+
budgetState: s.budgetState,
|
|
587
|
+
maxBridgeCalls: s.maxBridgeCalls,
|
|
588
|
+
activeHostTimers: s.activeHostTimers,
|
|
589
|
+
sendStreamEvent,
|
|
590
|
+
});
|
|
591
|
+
const kernelHandleDispatchHandlers = buildKernelHandleDispatchHandlers({
|
|
592
|
+
processTable: this.processTable,
|
|
593
|
+
pid: this.pid ?? 1,
|
|
594
|
+
budgetState: s.budgetState,
|
|
595
|
+
maxBridgeCalls: s.maxBridgeCalls,
|
|
596
|
+
});
|
|
597
|
+
const bridgeHandlers = {
|
|
598
|
+
...cryptoResult.handlers,
|
|
599
|
+
...buildConsoleBridgeHandlers({
|
|
600
|
+
onStdio,
|
|
601
|
+
budgetState: s.budgetState,
|
|
602
|
+
maxOutputBytes: s.maxOutputBytes,
|
|
603
|
+
}),
|
|
604
|
+
...buildModuleLoadingBridgeHandlers({
|
|
605
|
+
filesystem: s.filesystem,
|
|
606
|
+
resolutionCache: s.resolutionCache,
|
|
607
|
+
resolveMode: entryIsEsm ? "import" : "require",
|
|
608
|
+
sandboxToHostPath: (p) => {
|
|
609
|
+
const rfs = this.rawFilesystem;
|
|
610
|
+
return typeof rfs?.toHostPath === "function" ? rfs.toHostPath(p) : null;
|
|
611
|
+
},
|
|
612
|
+
}, {
|
|
613
|
+
// Dispatch handlers routed through _loadPolyfill for V8 runtime compat
|
|
614
|
+
...cryptoResult.handlers,
|
|
615
|
+
...networkBridgeResult.handlers,
|
|
616
|
+
...netSocketResult.handlers,
|
|
617
|
+
...buildModuleResolutionBridgeHandlers({
|
|
618
|
+
sandboxToHostPath: (p) => {
|
|
619
|
+
const fs = s.filesystem;
|
|
620
|
+
return typeof fs.toHostPath === "function" ? fs.toHostPath(p) : null;
|
|
621
|
+
},
|
|
622
|
+
hostToSandboxPath: (p) => {
|
|
623
|
+
const fs = s.filesystem;
|
|
624
|
+
return typeof fs.toSandboxPath === "function" ? fs.toSandboxPath(p) : p;
|
|
625
|
+
},
|
|
626
|
+
}),
|
|
627
|
+
...buildPtyBridgeHandlers({
|
|
628
|
+
onPtySetRawMode: s.onPtySetRawMode,
|
|
629
|
+
stdinIsTTY: s.processConfig.stdinIsTTY,
|
|
630
|
+
}),
|
|
631
|
+
// Kernel FD table handlers
|
|
632
|
+
...kernelFdResult.handlers,
|
|
633
|
+
...kernelTimerDispatchHandlers,
|
|
634
|
+
...kernelHandleDispatchHandlers,
|
|
635
|
+
// Custom bindings dispatched through _loadPolyfill
|
|
636
|
+
...(this.flattenedBindings ? Object.fromEntries(this.flattenedBindings.map(b => [b.key, b.handler])) : {}),
|
|
637
|
+
}),
|
|
638
|
+
...buildTimerBridgeHandlers({
|
|
639
|
+
budgetState: s.budgetState,
|
|
640
|
+
maxBridgeCalls: s.maxBridgeCalls,
|
|
641
|
+
activeHostTimers: s.activeHostTimers,
|
|
642
|
+
}),
|
|
643
|
+
...buildFsBridgeHandlers({
|
|
644
|
+
filesystem: s.filesystem,
|
|
645
|
+
budgetState: s.budgetState,
|
|
646
|
+
maxBridgeCalls: s.maxBridgeCalls,
|
|
647
|
+
bridgeBase64TransferLimitBytes: s.bridgeBase64TransferLimitBytes,
|
|
648
|
+
isolateJsonPayloadLimitBytes: s.isolateJsonPayloadLimitBytes,
|
|
649
|
+
}),
|
|
650
|
+
...buildChildProcessBridgeHandlers({
|
|
651
|
+
commandExecutor: s.commandExecutor,
|
|
652
|
+
processConfig: s.processConfig,
|
|
653
|
+
budgetState: s.budgetState,
|
|
654
|
+
maxBridgeCalls: s.maxBridgeCalls,
|
|
655
|
+
maxChildProcesses: s.maxChildProcesses,
|
|
656
|
+
isolateJsonPayloadLimitBytes: s.isolateJsonPayloadLimitBytes,
|
|
657
|
+
activeChildProcesses: s.activeChildProcesses,
|
|
658
|
+
sendStreamEvent,
|
|
659
|
+
processTable: this.processTable,
|
|
660
|
+
parentPid: this.pid,
|
|
661
|
+
}),
|
|
662
|
+
...networkBridgeResult.handlers,
|
|
663
|
+
...netSocketResult.handlers,
|
|
664
|
+
...buildModuleResolutionBridgeHandlers({
|
|
665
|
+
sandboxToHostPath: (p) => {
|
|
666
|
+
const rfs = this.rawFilesystem;
|
|
667
|
+
return typeof rfs?.toHostPath === "function" ? rfs.toHostPath(p) : null;
|
|
668
|
+
},
|
|
669
|
+
hostToSandboxPath: (p) => {
|
|
670
|
+
const rfs = this.rawFilesystem;
|
|
671
|
+
return typeof rfs?.toSandboxPath === "function" ? rfs.toSandboxPath(p) : p;
|
|
672
|
+
},
|
|
673
|
+
}),
|
|
674
|
+
...buildPtyBridgeHandlers({
|
|
675
|
+
onPtySetRawMode: s.onPtySetRawMode,
|
|
676
|
+
stdinIsTTY: s.processConfig.stdinIsTTY,
|
|
677
|
+
}),
|
|
678
|
+
};
|
|
679
|
+
// Merge custom bindings into bridge handlers
|
|
680
|
+
if (this.flattenedBindings) {
|
|
681
|
+
for (const binding of this.flattenedBindings) {
|
|
682
|
+
bridgeHandlers[binding.key] = binding.handler;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Build bridge code with embedded config
|
|
686
|
+
const bridgeCode = buildFullBridgeCode();
|
|
687
|
+
// Build post-restore script with per-execution config
|
|
688
|
+
const bindingKeys = this.flattenedBindings
|
|
689
|
+
? this.flattenedBindings.map((b) => b.key.slice(BINDING_PREFIX.length))
|
|
690
|
+
: [];
|
|
691
|
+
const postRestoreScript = buildPostRestoreScript(execProcessConfig, s.osConfig, {
|
|
692
|
+
initialCwd: execProcessConfig.cwd ?? "/",
|
|
693
|
+
jsonPayloadLimitBytes: s.isolateJsonPayloadLimitBytes,
|
|
694
|
+
payloadLimitErrorCode: PAYLOAD_LIMIT_ERROR_CODE,
|
|
695
|
+
maxTimers: s.maxTimers,
|
|
696
|
+
maxHandles: s.maxHandles,
|
|
697
|
+
stdin: options.stdin,
|
|
698
|
+
}, timingMitigation, frozenTimeMs, options.mode, options.filePath, bindingKeys);
|
|
699
|
+
// Execute in V8 session
|
|
700
|
+
const result = await session.execute({
|
|
701
|
+
bridgeCode,
|
|
702
|
+
postRestoreScript,
|
|
703
|
+
userCode,
|
|
704
|
+
mode: sessionMode,
|
|
705
|
+
filePath: options.filePath,
|
|
706
|
+
processConfig: {
|
|
707
|
+
cwd: execProcessConfig.cwd ?? "/",
|
|
708
|
+
env: execProcessConfig.env ?? {},
|
|
709
|
+
timing_mitigation: timingMitigation,
|
|
710
|
+
frozen_time_ms: timingMitigation === "freeze" ? frozenTimeMs : null,
|
|
711
|
+
},
|
|
712
|
+
osConfig: {
|
|
713
|
+
homedir: s.osConfig.homedir ?? DEFAULT_SANDBOX_HOME,
|
|
714
|
+
tmpdir: s.osConfig.tmpdir ?? DEFAULT_SANDBOX_TMPDIR,
|
|
715
|
+
platform: s.osConfig.platform ?? "linux",
|
|
716
|
+
arch: s.osConfig.arch ?? "x64",
|
|
717
|
+
},
|
|
718
|
+
bridgeHandlers,
|
|
719
|
+
onStreamCallback: (callbackType, payload) => {
|
|
720
|
+
// Handle stream callbacks from V8 isolate
|
|
721
|
+
if (callbackType === "httpServerResponse") {
|
|
722
|
+
try {
|
|
723
|
+
const data = JSON.parse(Buffer.from(payload).toString());
|
|
724
|
+
resolveHttpServerResponse({
|
|
725
|
+
requestId: data.requestId !== undefined
|
|
726
|
+
? Number(data.requestId)
|
|
727
|
+
: undefined,
|
|
728
|
+
serverId: data.serverId !== undefined
|
|
729
|
+
? Number(data.serverId)
|
|
730
|
+
: undefined,
|
|
731
|
+
responseJson: data.responseJson,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// Invalid payload
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
if (options.mode === "exec" && !result.error) {
|
|
741
|
+
await this.waitForManagedResources();
|
|
742
|
+
}
|
|
743
|
+
// Clean up per-execution resources
|
|
744
|
+
cryptoResult.dispose();
|
|
745
|
+
netSocketResult.dispose();
|
|
746
|
+
kernelFdResult.dispose();
|
|
747
|
+
await networkBridgeResult.dispose();
|
|
748
|
+
// Map V8 execution result to RunResult
|
|
749
|
+
if (result.error) {
|
|
750
|
+
const errMessage = result.error.type && result.error.type !== "Error"
|
|
751
|
+
? `${result.error.type}: ${result.error.message}`
|
|
752
|
+
: result.error.message;
|
|
753
|
+
// Check for timeout
|
|
754
|
+
if (/timed out|time limit exceeded/i.test(errMessage)) {
|
|
755
|
+
finalExitCode = TIMEOUT_EXIT_CODE;
|
|
756
|
+
return {
|
|
757
|
+
code: TIMEOUT_EXIT_CODE,
|
|
758
|
+
errorMessage: TIMEOUT_ERROR_MESSAGE,
|
|
759
|
+
exports: undefined,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
// Check for process.exit()
|
|
763
|
+
const exitMatch = errMessage.match(/process\.exit\((\d+)\)/);
|
|
764
|
+
if (exitMatch) {
|
|
765
|
+
finalExitCode = parseInt(exitMatch[1], 10);
|
|
766
|
+
return {
|
|
767
|
+
code: finalExitCode,
|
|
768
|
+
exports: undefined,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
finalExitCode = result.code || 1;
|
|
772
|
+
return {
|
|
773
|
+
code: finalExitCode,
|
|
774
|
+
errorMessage: boundErrorMessage(errMessage),
|
|
775
|
+
exports: undefined,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
// Parse exports for run() mode
|
|
779
|
+
let exports;
|
|
780
|
+
if (options.mode === "run" && result.exports) {
|
|
781
|
+
try {
|
|
782
|
+
const { deserialize } = await import("node:v8");
|
|
783
|
+
exports = deserialize(result.exports);
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
exports = undefined;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
finalExitCode = result.code;
|
|
790
|
+
return {
|
|
791
|
+
code: finalExitCode,
|
|
792
|
+
exports,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
const errMessage = err instanceof Error
|
|
797
|
+
? (err.name && err.name !== "Error" ? `${err.name}: ${err.message}` : err.message)
|
|
798
|
+
: String(err);
|
|
799
|
+
if (/timed out|time limit exceeded/i.test(errMessage)) {
|
|
800
|
+
finalExitCode = TIMEOUT_EXIT_CODE;
|
|
801
|
+
return {
|
|
802
|
+
code: TIMEOUT_EXIT_CODE,
|
|
803
|
+
errorMessage: TIMEOUT_ERROR_MESSAGE,
|
|
804
|
+
exports: undefined,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const exitMatch = errMessage.match(/process\.exit\((\d+)\)/);
|
|
808
|
+
if (exitMatch) {
|
|
809
|
+
finalExitCode = parseInt(exitMatch[1], 10);
|
|
810
|
+
return {
|
|
811
|
+
code: finalExitCode,
|
|
812
|
+
exports: undefined,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
finalExitCode = 1;
|
|
816
|
+
return {
|
|
817
|
+
code: finalExitCode,
|
|
818
|
+
errorMessage: boundErrorMessage(errMessage),
|
|
819
|
+
exports: undefined,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
finally {
|
|
823
|
+
await session.destroy().catch(() => { });
|
|
824
|
+
this.finalizeExecutionState(finalExitCode);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
dispose() {
|
|
828
|
+
if (this.disposed)
|
|
829
|
+
return;
|
|
830
|
+
this.disposed = true;
|
|
831
|
+
killActiveChildProcesses(this.state);
|
|
832
|
+
clearActiveHostTimers(this.state);
|
|
833
|
+
if (this.pid !== undefined) {
|
|
834
|
+
this.clearKernelTimersForProcess(this.pid);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async terminate() {
|
|
838
|
+
if (this.disposed)
|
|
839
|
+
return;
|
|
840
|
+
killActiveChildProcesses(this.state);
|
|
841
|
+
const closers = Array.from(this.state.activeHttpServerClosers.values());
|
|
842
|
+
await Promise.allSettled(closers.map((close) => close()));
|
|
843
|
+
this.state.activeHttpServerIds.clear();
|
|
844
|
+
this.state.activeHttpServerClosers.clear();
|
|
845
|
+
clearActiveHostTimers(this.state);
|
|
846
|
+
if (this.pid !== undefined) {
|
|
847
|
+
this.clearKernelTimersForProcess(this.pid);
|
|
848
|
+
}
|
|
849
|
+
this.disposed = true;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/** Build the post-restore script that configures the V8 session per-execution. */
|
|
853
|
+
function buildPostRestoreScript(processConfig, osConfig, bridgeConfig, timingMitigation, frozenTimeMs, mode, filePath, bindingKeys) {
|
|
854
|
+
const parts = [];
|
|
855
|
+
// Shim existing native bridge functions for ivm.Reference compat,
|
|
856
|
+
// then install dispatch wrappers for bridge globals not in the V8 binary
|
|
857
|
+
parts.push(BRIDGE_NATIVE_SHIM);
|
|
858
|
+
parts.push(BRIDGE_DISPATCH_SHIM);
|
|
859
|
+
// Console and require setup (must run in postRestoreScript, not bridgeCode,
|
|
860
|
+
// because bridge calls are muted during the bridgeCode snapshot phase)
|
|
861
|
+
parts.push(getConsoleSetupCode());
|
|
862
|
+
parts.push(getRequireSetupCode());
|
|
863
|
+
parts.push(getIsolateRuntimeSource("setupFsFacade"));
|
|
864
|
+
parts.push(`globalThis.__runtimeDynamicImportConfig = ${JSON.stringify({
|
|
865
|
+
referrerPath: filePath ?? processConfig.cwd ?? bridgeConfig.initialCwd,
|
|
866
|
+
})};`);
|
|
867
|
+
parts.push(getIsolateRuntimeSource("setupDynamicImport"));
|
|
868
|
+
// Inject bridge setup config
|
|
869
|
+
parts.push(`globalThis.__runtimeBridgeSetupConfig = ${JSON.stringify({
|
|
870
|
+
initialCwd: bridgeConfig.initialCwd,
|
|
871
|
+
jsonPayloadLimitBytes: bridgeConfig.jsonPayloadLimitBytes,
|
|
872
|
+
payloadLimitErrorCode: bridgeConfig.payloadLimitErrorCode,
|
|
873
|
+
})};`);
|
|
874
|
+
// Inject process and OS config
|
|
875
|
+
parts.push(`globalThis.${getProcessConfigGlobalKey()} = ${JSON.stringify(processConfig)};`);
|
|
876
|
+
parts.push(`globalThis.${getOsConfigGlobalKey()} = ${JSON.stringify(osConfig)};`);
|
|
877
|
+
// Inject TTY config separately — InjectGlobals overwrites _processConfig,
|
|
878
|
+
// so TTY flags need their own global that persists
|
|
879
|
+
if (processConfig.stdinIsTTY || processConfig.stdoutIsTTY || processConfig.stderrIsTTY) {
|
|
880
|
+
parts.push(`globalThis.__runtimeTtyConfig = ${JSON.stringify({
|
|
881
|
+
stdinIsTTY: processConfig.stdinIsTTY,
|
|
882
|
+
stdoutIsTTY: processConfig.stdoutIsTTY,
|
|
883
|
+
stderrIsTTY: processConfig.stderrIsTTY,
|
|
884
|
+
})};`);
|
|
885
|
+
}
|
|
886
|
+
// Inject timer/handle limits
|
|
887
|
+
if (bridgeConfig.maxTimers !== undefined) {
|
|
888
|
+
parts.push(`globalThis._maxTimers = ${bridgeConfig.maxTimers};`);
|
|
889
|
+
}
|
|
890
|
+
if (bridgeConfig.maxHandles !== undefined) {
|
|
891
|
+
parts.push(`globalThis._maxHandles = ${bridgeConfig.maxHandles};`);
|
|
892
|
+
}
|
|
893
|
+
// Apply timing mitigation
|
|
894
|
+
if (timingMitigation === "freeze") {
|
|
895
|
+
parts.push(`globalThis.__runtimeTimingMitigationConfig = ${JSON.stringify({ frozenTimeMs })};`);
|
|
896
|
+
parts.push(getIsolateRuntimeSource("applyTimingMitigationFreeze"));
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
parts.push(getIsolateRuntimeSource("applyTimingMitigationOff"));
|
|
900
|
+
}
|
|
901
|
+
// Apply execution overrides (env, cwd, stdin) for exec mode
|
|
902
|
+
if (mode === "exec") {
|
|
903
|
+
if (processConfig.env) {
|
|
904
|
+
parts.push(`globalThis.__runtimeProcessEnvOverride = ${JSON.stringify(processConfig.env)};`);
|
|
905
|
+
parts.push(getIsolateRuntimeSource("overrideProcessEnv"));
|
|
906
|
+
}
|
|
907
|
+
if (processConfig.cwd) {
|
|
908
|
+
parts.push(`globalThis.__runtimeProcessCwdOverride = ${JSON.stringify(processConfig.cwd)};`);
|
|
909
|
+
parts.push(getIsolateRuntimeSource("overrideProcessCwd"));
|
|
910
|
+
}
|
|
911
|
+
if (bridgeConfig.stdin !== undefined) {
|
|
912
|
+
parts.push(`globalThis.__runtimeStdinData = ${JSON.stringify(bridgeConfig.stdin)};`);
|
|
913
|
+
parts.push(getIsolateRuntimeSource("setStdinData"));
|
|
914
|
+
}
|
|
915
|
+
// Set CommonJS globals
|
|
916
|
+
parts.push(getIsolateRuntimeSource("initCommonjsModuleGlobals"));
|
|
917
|
+
if (filePath) {
|
|
918
|
+
const dirname = filePath.includes("/")
|
|
919
|
+
? filePath.substring(0, filePath.lastIndexOf("/")) || "/"
|
|
920
|
+
: "/";
|
|
921
|
+
parts.push(`globalThis.__runtimeCommonJsFileConfig = ${JSON.stringify({ filePath, dirname })};`);
|
|
922
|
+
parts.push(getIsolateRuntimeSource("setCommonjsFileGlobals"));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
// run mode — still need CommonJS module globals
|
|
927
|
+
parts.push(getIsolateRuntimeSource("initCommonjsModuleGlobals"));
|
|
928
|
+
if (filePath) {
|
|
929
|
+
const dirname = filePath.includes("/")
|
|
930
|
+
? filePath.substring(0, filePath.lastIndexOf("/")) || "/"
|
|
931
|
+
: "/";
|
|
932
|
+
parts.push(`globalThis.__runtimeCommonJsFileConfig = ${JSON.stringify({ filePath, dirname })};`);
|
|
933
|
+
parts.push(getIsolateRuntimeSource("setCommonjsFileGlobals"));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// Apply custom global exposure policy
|
|
937
|
+
parts.push(`globalThis.__runtimeCustomGlobalPolicy = ${JSON.stringify({
|
|
938
|
+
hardenedGlobals: getHardenedGlobals(),
|
|
939
|
+
mutableGlobals: getMutableGlobals(),
|
|
940
|
+
})};`);
|
|
941
|
+
parts.push(getIsolateRuntimeSource("applyCustomGlobalPolicy"));
|
|
942
|
+
// Inflate SecureExec.bindings from flattened __bind.* globals
|
|
943
|
+
parts.push(buildBindingsInflationSnippet(bindingKeys ?? []));
|
|
944
|
+
return parts.join("\n");
|
|
945
|
+
}
|
|
946
|
+
// Import global exposure policy constants
|
|
947
|
+
import { HARDENED_NODE_CUSTOM_GLOBALS, MUTABLE_NODE_CUSTOM_GLOBALS, } from "@secure-exec/core/internal/shared/global-exposure";
|
|
948
|
+
import { HOST_BRIDGE_GLOBAL_KEYS, } from "./bridge-contract.js";
|
|
949
|
+
function getHardenedGlobals() { return HARDENED_NODE_CUSTOM_GLOBALS; }
|
|
950
|
+
function getMutableGlobals() { return MUTABLE_NODE_CUSTOM_GLOBALS; }
|
|
951
|
+
function getProcessConfigGlobalKey() { return HOST_BRIDGE_GLOBAL_KEYS.processConfig; }
|
|
952
|
+
function getOsConfigGlobalKey() { return HOST_BRIDGE_GLOBAL_KEYS.osConfig; }
|
|
953
|
+
/** Build the JS snippet that inflates __bind.* globals into a frozen SecureExec.bindings tree. */
|
|
954
|
+
function buildBindingsInflationSnippet(bindingKeys) {
|
|
955
|
+
// Build dispatch wrappers for each binding key and assign directly to the
|
|
956
|
+
// tree nodes. Uses _loadPolyfill as the dispatch multiplexer (same as the
|
|
957
|
+
// static dispatch shim for internal bridge globals).
|
|
958
|
+
return `(function(){
|
|
959
|
+
var __bindingKeys__=${JSON.stringify(bindingKeys)};
|
|
960
|
+
var tree={};
|
|
961
|
+
function makeBindFn(bk){
|
|
962
|
+
return function(){var args=Array.prototype.slice.call(arguments);var encoded="__bd:"+bk+":"+JSON.stringify(args);var r=_loadPolyfill.applySyncPromise(undefined,[encoded]);if(r===null)return undefined;try{var p=JSON.parse(r);if(p.__bd_error)throw new Error(p.__bd_error);return p.__bd_result;}catch(e){if(e.message&&e.message.startsWith("No handler:"))return undefined;throw e;}};
|
|
963
|
+
}
|
|
964
|
+
for(var i=0;i<__bindingKeys__.length;i++){
|
|
965
|
+
var parts=__bindingKeys__[i].split(".");
|
|
966
|
+
var node=tree;
|
|
967
|
+
for(var j=0;j<parts.length-1;j++){node[parts[j]]=node[parts[j]]||{};node=node[parts[j]];}
|
|
968
|
+
node[parts[parts.length-1]]=makeBindFn("__bind."+__bindingKeys__[i]);
|
|
969
|
+
}
|
|
970
|
+
function deepFreeze(obj){
|
|
971
|
+
var vals=Object.values(obj);
|
|
972
|
+
for(var k=0;k<vals.length;k++){if(typeof vals[k]==="object"&&vals[k]!==null)deepFreeze(vals[k]);}
|
|
973
|
+
return Object.freeze(obj);
|
|
974
|
+
}
|
|
975
|
+
Object.defineProperty(globalThis,"SecureExec",{value:Object.freeze({bindings:deepFreeze(tree)}),writable:false,enumerable:true,configurable:false});
|
|
976
|
+
})();`;
|
|
977
|
+
}
|