@secure-exec/core 0.1.0-rc.1 → 0.1.0-rc.3
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/dist/bridge/active-handles.d.ts +1 -0
- package/dist/bridge/active-handles.js +5 -10
- package/dist/bridge/child-process.d.ts +9 -0
- package/dist/bridge/child-process.js +71 -7
- package/dist/bridge/fs.js +96 -12
- package/dist/bridge/network.js +7 -7
- package/dist/bridge/process.js +72 -34
- package/dist/bridge.js +264 -53
- package/dist/generated/isolate-runtime.d.ts +3 -3
- package/dist/generated/isolate-runtime.js +3 -3
- package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +102 -18
- package/dist/isolate-runtime/bridge-initial-globals.js +2 -2
- package/dist/isolate-runtime/require-setup.js +72 -36
- package/dist/runtime-driver.d.ts +2 -0
- package/dist/shared/global-exposure.js +27 -2
- package/dist/shared/permissions.js +51 -21
- package/package.json +1 -1
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { exposeCustomGlobal } from "../shared/global-exposure.js";
|
|
2
|
-
/**
|
|
3
|
-
* Active Handles: Mechanism to keep the sandbox alive for async operations.
|
|
4
|
-
*
|
|
5
|
-
* isolated-vm doesn't have an event loop, so async callbacks (like child process
|
|
6
|
-
* events) would never fire because the sandbox exits immediately after synchronous
|
|
7
|
-
* code finishes. This module tracks active handles and provides a promise that
|
|
8
|
-
* resolves when all handles complete.
|
|
9
|
-
*
|
|
10
|
-
* See: docs-internal/node/ACTIVE_HANDLES.md
|
|
11
|
-
*/
|
|
12
2
|
// Map of active handles: id -> description (for debugging)
|
|
13
3
|
const _activeHandles = new Map();
|
|
14
4
|
// Resolvers waiting for all handles to complete
|
|
15
5
|
let _waitResolvers = [];
|
|
16
6
|
/**
|
|
17
7
|
* Register an active handle that keeps the sandbox alive.
|
|
8
|
+
* Throws if the handle cap (_maxHandles) would be exceeded.
|
|
18
9
|
* @param id Unique identifier for the handle
|
|
19
10
|
* @param description Human-readable description for debugging
|
|
20
11
|
*/
|
|
21
12
|
export function _registerHandle(id, description) {
|
|
13
|
+
// Enforce handle cap (skip check for re-registration of existing handle)
|
|
14
|
+
if (typeof _maxHandles !== "undefined" && !_activeHandles.has(id) && _activeHandles.size >= _maxHandles) {
|
|
15
|
+
throw new Error("ERR_RESOURCE_BUDGET_EXCEEDED: maximum active handles exceeded");
|
|
16
|
+
}
|
|
22
17
|
_activeHandles.set(id, description);
|
|
23
18
|
}
|
|
24
19
|
/**
|
|
@@ -12,11 +12,15 @@ interface OutputStreamStub {
|
|
|
12
12
|
readable: boolean;
|
|
13
13
|
_listeners: Record<string, EventListener[]>;
|
|
14
14
|
_onceListeners: Record<string, EventListener[]>;
|
|
15
|
+
_maxListeners: number;
|
|
16
|
+
_maxListenersWarned: Set<string>;
|
|
15
17
|
on(event: string, listener: EventListener): OutputStreamStub;
|
|
16
18
|
once(event: string, listener: EventListener): OutputStreamStub;
|
|
17
19
|
emit(event: string, ...args: unknown[]): boolean;
|
|
18
20
|
read(): null;
|
|
19
21
|
setEncoding(): OutputStreamStub;
|
|
22
|
+
setMaxListeners(n: number): OutputStreamStub;
|
|
23
|
+
getMaxListeners(): number;
|
|
20
24
|
pipe<T extends NodeJS.WritableStream>(dest: T): T;
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
@@ -27,6 +31,8 @@ interface OutputStreamStub {
|
|
|
27
31
|
declare class ChildProcess {
|
|
28
32
|
private _listeners;
|
|
29
33
|
private _onceListeners;
|
|
34
|
+
private _maxListeners;
|
|
35
|
+
private _maxListenersWarned;
|
|
30
36
|
pid: number;
|
|
31
37
|
killed: boolean;
|
|
32
38
|
exitCode: number | null;
|
|
@@ -43,6 +49,9 @@ declare class ChildProcess {
|
|
|
43
49
|
once(event: string, listener: EventListener): this;
|
|
44
50
|
off(event: string, listener: EventListener): this;
|
|
45
51
|
removeListener(event: string, listener: EventListener): this;
|
|
52
|
+
setMaxListeners(n: number): this;
|
|
53
|
+
getMaxListeners(): number;
|
|
54
|
+
private _checkMaxListeners;
|
|
46
55
|
emit(event: string, ...args: unknown[]): boolean;
|
|
47
56
|
kill(_signal?: NodeJS.Signals | number): boolean;
|
|
48
57
|
ref(): this;
|
|
@@ -38,6 +38,21 @@ const childProcessDispatch = (sessionId, type, data) => {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
exposeCustomGlobal("_childProcessDispatch", childProcessDispatch);
|
|
41
|
+
/** Warn when listener count exceeds max (Node.js: warn, don't crash) */
|
|
42
|
+
function checkStreamMaxListeners(stream, event) {
|
|
43
|
+
if (stream._maxListeners > 0 && !stream._maxListenersWarned.has(event)) {
|
|
44
|
+
const total = (stream._listeners[event]?.length ?? 0) + (stream._onceListeners[event]?.length ?? 0);
|
|
45
|
+
if (total > stream._maxListeners) {
|
|
46
|
+
stream._maxListenersWarned.add(event);
|
|
47
|
+
const warning = `MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${total} ${event} listeners added. MaxListeners is ${stream._maxListeners}. Use emitter.setMaxListeners() to increase limit`;
|
|
48
|
+
if (typeof console !== "undefined" && console.error) {
|
|
49
|
+
console.error(warning);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Monotonic counter for unique ChildProcess PIDs
|
|
55
|
+
let _nextChildPid = 1000;
|
|
41
56
|
/**
|
|
42
57
|
* Polyfill of Node.js `ChildProcess`. Provides event-emitting stdin/stdout/stderr
|
|
43
58
|
* streams. In streaming mode, data arrives via the `_childProcessDispatch` global
|
|
@@ -46,7 +61,9 @@ exposeCustomGlobal("_childProcessDispatch", childProcessDispatch);
|
|
|
46
61
|
class ChildProcess {
|
|
47
62
|
_listeners = {};
|
|
48
63
|
_onceListeners = {};
|
|
49
|
-
|
|
64
|
+
_maxListeners = 10;
|
|
65
|
+
_maxListenersWarned = new Set();
|
|
66
|
+
pid = _nextChildPid++;
|
|
50
67
|
killed = false;
|
|
51
68
|
exitCode = null;
|
|
52
69
|
signalCode = null;
|
|
@@ -82,16 +99,20 @@ class ChildProcess {
|
|
|
82
99
|
readable: true,
|
|
83
100
|
_listeners: {},
|
|
84
101
|
_onceListeners: {},
|
|
102
|
+
_maxListeners: 10,
|
|
103
|
+
_maxListenersWarned: new Set(),
|
|
85
104
|
on(event, listener) {
|
|
86
105
|
if (!this._listeners[event])
|
|
87
106
|
this._listeners[event] = [];
|
|
88
107
|
this._listeners[event].push(listener);
|
|
108
|
+
checkStreamMaxListeners(this, event);
|
|
89
109
|
return this;
|
|
90
110
|
},
|
|
91
111
|
once(event, listener) {
|
|
92
112
|
if (!this._onceListeners[event])
|
|
93
113
|
this._onceListeners[event] = [];
|
|
94
114
|
this._onceListeners[event].push(listener);
|
|
115
|
+
checkStreamMaxListeners(this, event);
|
|
95
116
|
return this;
|
|
96
117
|
},
|
|
97
118
|
emit(event, ...args) {
|
|
@@ -110,6 +131,13 @@ class ChildProcess {
|
|
|
110
131
|
setEncoding() {
|
|
111
132
|
return this;
|
|
112
133
|
},
|
|
134
|
+
setMaxListeners(n) {
|
|
135
|
+
this._maxListeners = n;
|
|
136
|
+
return this;
|
|
137
|
+
},
|
|
138
|
+
getMaxListeners() {
|
|
139
|
+
return this._maxListeners;
|
|
140
|
+
},
|
|
113
141
|
pipe(dest) {
|
|
114
142
|
return dest;
|
|
115
143
|
},
|
|
@@ -119,16 +147,20 @@ class ChildProcess {
|
|
|
119
147
|
readable: true,
|
|
120
148
|
_listeners: {},
|
|
121
149
|
_onceListeners: {},
|
|
150
|
+
_maxListeners: 10,
|
|
151
|
+
_maxListenersWarned: new Set(),
|
|
122
152
|
on(event, listener) {
|
|
123
153
|
if (!this._listeners[event])
|
|
124
154
|
this._listeners[event] = [];
|
|
125
155
|
this._listeners[event].push(listener);
|
|
156
|
+
checkStreamMaxListeners(this, event);
|
|
126
157
|
return this;
|
|
127
158
|
},
|
|
128
159
|
once(event, listener) {
|
|
129
160
|
if (!this._onceListeners[event])
|
|
130
161
|
this._onceListeners[event] = [];
|
|
131
162
|
this._onceListeners[event].push(listener);
|
|
163
|
+
checkStreamMaxListeners(this, event);
|
|
132
164
|
return this;
|
|
133
165
|
},
|
|
134
166
|
emit(event, ...args) {
|
|
@@ -147,6 +179,13 @@ class ChildProcess {
|
|
|
147
179
|
setEncoding() {
|
|
148
180
|
return this;
|
|
149
181
|
},
|
|
182
|
+
setMaxListeners(n) {
|
|
183
|
+
this._maxListeners = n;
|
|
184
|
+
return this;
|
|
185
|
+
},
|
|
186
|
+
getMaxListeners() {
|
|
187
|
+
return this._maxListeners;
|
|
188
|
+
},
|
|
150
189
|
pipe(dest) {
|
|
151
190
|
return dest;
|
|
152
191
|
},
|
|
@@ -157,12 +196,14 @@ class ChildProcess {
|
|
|
157
196
|
if (!this._listeners[event])
|
|
158
197
|
this._listeners[event] = [];
|
|
159
198
|
this._listeners[event].push(listener);
|
|
199
|
+
this._checkMaxListeners(event);
|
|
160
200
|
return this;
|
|
161
201
|
}
|
|
162
202
|
once(event, listener) {
|
|
163
203
|
if (!this._onceListeners[event])
|
|
164
204
|
this._onceListeners[event] = [];
|
|
165
205
|
this._onceListeners[event].push(listener);
|
|
206
|
+
this._checkMaxListeners(event);
|
|
166
207
|
return this;
|
|
167
208
|
}
|
|
168
209
|
off(event, listener) {
|
|
@@ -176,6 +217,25 @@ class ChildProcess {
|
|
|
176
217
|
removeListener(event, listener) {
|
|
177
218
|
return this.off(event, listener);
|
|
178
219
|
}
|
|
220
|
+
setMaxListeners(n) {
|
|
221
|
+
this._maxListeners = n;
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
getMaxListeners() {
|
|
225
|
+
return this._maxListeners;
|
|
226
|
+
}
|
|
227
|
+
_checkMaxListeners(event) {
|
|
228
|
+
if (this._maxListeners > 0 && !this._maxListenersWarned.has(event)) {
|
|
229
|
+
const total = (this._listeners[event]?.length ?? 0) + (this._onceListeners[event]?.length ?? 0);
|
|
230
|
+
if (total > this._maxListeners) {
|
|
231
|
+
this._maxListenersWarned.add(event);
|
|
232
|
+
const warning = `MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${total} ${event} listeners added to [ChildProcess]. MaxListeners is ${this._maxListeners}. Use emitter.setMaxListeners() to increase limit`;
|
|
233
|
+
if (typeof console !== "undefined" && console.error) {
|
|
234
|
+
console.error(warning);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
179
239
|
emit(event, ...args) {
|
|
180
240
|
let handled = false;
|
|
181
241
|
if (this._listeners[event]) {
|
|
@@ -248,19 +308,23 @@ function exec(command, options, callback) {
|
|
|
248
308
|
let stderrBytes = 0;
|
|
249
309
|
let maxBufferExceeded = false;
|
|
250
310
|
child.stdout.on("data", (data) => {
|
|
311
|
+
if (maxBufferExceeded)
|
|
312
|
+
return;
|
|
251
313
|
const chunk = String(data);
|
|
252
314
|
stdout += chunk;
|
|
253
315
|
stdoutBytes += chunk.length;
|
|
254
|
-
if (stdoutBytes > maxBuffer
|
|
316
|
+
if (stdoutBytes > maxBuffer) {
|
|
255
317
|
maxBufferExceeded = true;
|
|
256
318
|
child.kill("SIGTERM");
|
|
257
319
|
}
|
|
258
320
|
});
|
|
259
321
|
child.stderr.on("data", (data) => {
|
|
322
|
+
if (maxBufferExceeded)
|
|
323
|
+
return;
|
|
260
324
|
const chunk = String(data);
|
|
261
325
|
stderr += chunk;
|
|
262
326
|
stderrBytes += chunk.length;
|
|
263
|
-
if (stderrBytes > maxBuffer
|
|
327
|
+
if (stderrBytes > maxBuffer) {
|
|
264
328
|
maxBufferExceeded = true;
|
|
265
329
|
child.kill("SIGTERM");
|
|
266
330
|
}
|
|
@@ -422,7 +486,7 @@ function spawnSync(command, args, options) {
|
|
|
422
486
|
}
|
|
423
487
|
if (typeof _childProcessSpawnSync === "undefined") {
|
|
424
488
|
return {
|
|
425
|
-
pid:
|
|
489
|
+
pid: _nextChildPid++,
|
|
426
490
|
output: [null, "", "child_process.spawnSync requires CommandExecutor to be configured"],
|
|
427
491
|
stdout: "",
|
|
428
492
|
stderr: "child_process.spawnSync requires CommandExecutor to be configured",
|
|
@@ -450,7 +514,7 @@ function spawnSync(command, args, options) {
|
|
|
450
514
|
const err = new Error("stdout maxBuffer length exceeded");
|
|
451
515
|
err.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
452
516
|
return {
|
|
453
|
-
pid:
|
|
517
|
+
pid: _nextChildPid++,
|
|
454
518
|
output: [null, stdoutBuf, stderrBuf],
|
|
455
519
|
stdout: stdoutBuf,
|
|
456
520
|
stderr: stderrBuf,
|
|
@@ -460,7 +524,7 @@ function spawnSync(command, args, options) {
|
|
|
460
524
|
};
|
|
461
525
|
}
|
|
462
526
|
return {
|
|
463
|
-
pid:
|
|
527
|
+
pid: _nextChildPid++,
|
|
464
528
|
output: [null, stdoutBuf, stderrBuf],
|
|
465
529
|
stdout: stdoutBuf,
|
|
466
530
|
stderr: stderrBuf,
|
|
@@ -473,7 +537,7 @@ function spawnSync(command, args, options) {
|
|
|
473
537
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
474
538
|
const stderrBuf = typeof Buffer !== "undefined" ? Buffer.from(errMsg) : errMsg;
|
|
475
539
|
return {
|
|
476
|
-
pid:
|
|
540
|
+
pid: _nextChildPid++,
|
|
477
541
|
output: [null, "", stderrBuf],
|
|
478
542
|
stdout: typeof Buffer !== "undefined" ? Buffer.from("") : "",
|
|
479
543
|
stderr: stderrBuf,
|
package/dist/bridge/fs.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// This module runs inside the isolate and provides Node.js fs API compatibility
|
|
3
3
|
// It communicates with the host via the _fs Reference object
|
|
4
4
|
import { Buffer } from "buffer";
|
|
5
|
-
// File descriptor table
|
|
5
|
+
// File descriptor table — capped to prevent resource exhaustion
|
|
6
|
+
const MAX_BRIDGE_FDS = 1024;
|
|
6
7
|
const fdTable = new Map();
|
|
7
8
|
let nextFd = 3;
|
|
8
9
|
const O_RDONLY = 0;
|
|
@@ -387,6 +388,7 @@ class ReadStream {
|
|
|
387
388
|
}
|
|
388
389
|
// WriteStream class for createWriteStream
|
|
389
390
|
// This provides a type-safe implementation that satisfies nodeFs.WriteStream
|
|
391
|
+
const MAX_WRITE_STREAM_BYTES = 16 * 1024 * 1024; // 16MB cap to prevent memory exhaustion
|
|
390
392
|
// We use 'as' assertion at the return site since the full interface is complex
|
|
391
393
|
class WriteStream {
|
|
392
394
|
// WriteStream-specific properties
|
|
@@ -452,6 +454,18 @@ class WriteStream {
|
|
|
452
454
|
else {
|
|
453
455
|
data = Buffer.from(String(chunk));
|
|
454
456
|
}
|
|
457
|
+
// Cap buffered data to prevent memory exhaustion
|
|
458
|
+
if (this.writableLength + data.length > MAX_WRITE_STREAM_BYTES) {
|
|
459
|
+
const err = new Error(`WriteStream buffer exceeded ${MAX_WRITE_STREAM_BYTES} bytes`);
|
|
460
|
+
this.errored = err;
|
|
461
|
+
this.destroyed = true;
|
|
462
|
+
this.writable = false;
|
|
463
|
+
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
464
|
+
if (cb)
|
|
465
|
+
Promise.resolve().then(() => cb(err));
|
|
466
|
+
Promise.resolve().then(() => this.emit("error", err));
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
455
469
|
this._chunks.push(data);
|
|
456
470
|
this.bytesWritten += data.length;
|
|
457
471
|
this.writableLength += data.length;
|
|
@@ -674,7 +688,7 @@ function canWrite(flags) {
|
|
|
674
688
|
function createFsError(code, message, syscall, path) {
|
|
675
689
|
const err = new Error(message);
|
|
676
690
|
err.code = code;
|
|
677
|
-
err.errno = code === "ENOENT" ? -2 : code === "EACCES" ? -13 : code === "EBADF" ? -9 : -1;
|
|
691
|
+
err.errno = code === "ENOENT" ? -2 : code === "EACCES" ? -13 : code === "EBADF" ? -9 : code === "EMFILE" ? -24 : -1;
|
|
678
692
|
err.syscall = syscall;
|
|
679
693
|
if (path)
|
|
680
694
|
err.path = path;
|
|
@@ -775,10 +789,13 @@ function _globGetBase(pattern) {
|
|
|
775
789
|
}
|
|
776
790
|
// Recursively walk VFS directory and collect matching paths
|
|
777
791
|
// We use a reference to `fs` via late-binding in the fs object method
|
|
792
|
+
const MAX_GLOB_DEPTH = 100; // Prevent stack overflow on deeply nested trees
|
|
778
793
|
function _globCollect(pattern, results) {
|
|
779
794
|
const regex = _globToRegex(pattern);
|
|
780
795
|
const base = _globGetBase(pattern);
|
|
781
|
-
const walk = (dir) => {
|
|
796
|
+
const walk = (dir, depth) => {
|
|
797
|
+
if (depth > MAX_GLOB_DEPTH)
|
|
798
|
+
return;
|
|
782
799
|
let entries;
|
|
783
800
|
try {
|
|
784
801
|
entries = _globReadDir(dir);
|
|
@@ -796,7 +813,7 @@ function _globCollect(pattern, results) {
|
|
|
796
813
|
try {
|
|
797
814
|
const stat = _globStat(fullPath);
|
|
798
815
|
if (stat.isDirectory()) {
|
|
799
|
-
walk(fullPath);
|
|
816
|
+
walk(fullPath, depth + 1);
|
|
800
817
|
}
|
|
801
818
|
}
|
|
802
819
|
catch {
|
|
@@ -814,7 +831,7 @@ function _globCollect(pattern, results) {
|
|
|
814
831
|
return;
|
|
815
832
|
}
|
|
816
833
|
}
|
|
817
|
-
walk(base);
|
|
834
|
+
walk(base, 0);
|
|
818
835
|
}
|
|
819
836
|
catch {
|
|
820
837
|
// Base doesn't exist — no matches
|
|
@@ -1123,6 +1140,10 @@ const fs = {
|
|
|
1123
1140
|
},
|
|
1124
1141
|
// File descriptor methods
|
|
1125
1142
|
openSync(path, flags, _mode) {
|
|
1143
|
+
// Enforce bridge-side FD limit
|
|
1144
|
+
if (fdTable.size >= MAX_BRIDGE_FDS) {
|
|
1145
|
+
throw createFsError("EMFILE", "EMFILE: too many open files, open '" + toPathString(path) + "'", "open", toPathString(path));
|
|
1146
|
+
}
|
|
1126
1147
|
const rawPath = toPathString(path);
|
|
1127
1148
|
const pathStr = rawPath;
|
|
1128
1149
|
const numFlags = parseFlags(flags);
|
|
@@ -1984,15 +2005,78 @@ const fs = {
|
|
|
1984
2005
|
}
|
|
1985
2006
|
},
|
|
1986
2007
|
realpathSync: Object.assign(function realpathSync(path) {
|
|
1987
|
-
//
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2008
|
+
// Resolve symlinks by walking each path component via lstat + readlink
|
|
2009
|
+
const MAX_SYMLINK_DEPTH = 40;
|
|
2010
|
+
let symlinksFollowed = 0;
|
|
2011
|
+
const raw = toPathString(path);
|
|
2012
|
+
// Build initial queue: normalize . and .. segments
|
|
2013
|
+
const pending = [];
|
|
2014
|
+
for (const seg of raw.split("/")) {
|
|
2015
|
+
if (!seg || seg === ".")
|
|
2016
|
+
continue;
|
|
2017
|
+
if (seg === "..") {
|
|
2018
|
+
if (pending.length > 0)
|
|
2019
|
+
pending.pop();
|
|
2020
|
+
}
|
|
2021
|
+
else
|
|
2022
|
+
pending.push(seg);
|
|
2023
|
+
}
|
|
2024
|
+
// Walk each component, resolving symlinks via a queue
|
|
2025
|
+
const resolved = [];
|
|
2026
|
+
while (pending.length > 0) {
|
|
2027
|
+
const seg = pending.shift();
|
|
2028
|
+
if (seg === ".")
|
|
2029
|
+
continue;
|
|
2030
|
+
if (seg === "..") {
|
|
2031
|
+
if (resolved.length > 0)
|
|
2032
|
+
resolved.pop();
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
resolved.push(seg);
|
|
2036
|
+
const currentPath = "/" + resolved.join("/");
|
|
2037
|
+
try {
|
|
2038
|
+
const stat = fs.lstatSync(currentPath);
|
|
2039
|
+
if (stat.isSymbolicLink()) {
|
|
2040
|
+
if (++symlinksFollowed > MAX_SYMLINK_DEPTH) {
|
|
2041
|
+
const err = new Error(`ELOOP: too many levels of symbolic links, realpath '${raw}'`);
|
|
2042
|
+
err.code = "ELOOP";
|
|
2043
|
+
err.syscall = "realpath";
|
|
2044
|
+
err.path = raw;
|
|
2045
|
+
throw err;
|
|
2046
|
+
}
|
|
2047
|
+
const target = fs.readlinkSync(currentPath);
|
|
2048
|
+
// Prepend target segments to pending for re-resolution
|
|
2049
|
+
const targetSegs = target.split("/").filter(Boolean);
|
|
2050
|
+
if (target.startsWith("/")) {
|
|
2051
|
+
// Absolute symlink — restart from root
|
|
2052
|
+
resolved.length = 0;
|
|
2053
|
+
}
|
|
2054
|
+
else {
|
|
2055
|
+
// Relative symlink — drop current component
|
|
2056
|
+
resolved.pop();
|
|
2057
|
+
}
|
|
2058
|
+
// Prepend target segments so they're processed next
|
|
2059
|
+
pending.unshift(...targetSegs);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
catch (e) {
|
|
2063
|
+
const err = e;
|
|
2064
|
+
if (err.code === "ELOOP")
|
|
2065
|
+
throw e;
|
|
2066
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
|
|
2067
|
+
const enoent = new Error(`ENOENT: no such file or directory, realpath '${raw}'`);
|
|
2068
|
+
enoent.code = "ENOENT";
|
|
2069
|
+
enoent.syscall = "realpath";
|
|
2070
|
+
enoent.path = raw;
|
|
2071
|
+
throw enoent;
|
|
2072
|
+
}
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
return "/" + resolved.join("/") || "/";
|
|
1991
2077
|
}, {
|
|
1992
2078
|
native(path) {
|
|
1993
|
-
return
|
|
1994
|
-
.replace(/\/\/+/g, "/")
|
|
1995
|
-
.replace(/\/$/, "") || "/";
|
|
2079
|
+
return fs.realpathSync(path);
|
|
1996
2080
|
}
|
|
1997
2081
|
}),
|
|
1998
2082
|
realpath: Object.assign(function realpath(path, callback) {
|
package/dist/bridge/network.js
CHANGED
|
@@ -1392,15 +1392,15 @@ exposeCustomGlobal("_httpsModule", https);
|
|
|
1392
1392
|
exposeCustomGlobal("_http2Module", http2);
|
|
1393
1393
|
exposeCustomGlobal("_dnsModule", dns);
|
|
1394
1394
|
exposeCustomGlobal("_httpServerDispatch", dispatchServerRequest);
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1395
|
+
// Harden fetch API globals (non-writable, non-configurable)
|
|
1396
|
+
exposeCustomGlobal("fetch", fetch);
|
|
1397
|
+
exposeCustomGlobal("Headers", Headers);
|
|
1398
|
+
exposeCustomGlobal("Request", Request);
|
|
1399
|
+
exposeCustomGlobal("Response", Response);
|
|
1400
1400
|
if (typeof globalThis.Blob === "undefined") {
|
|
1401
1401
|
// Minimal Blob stub used by server frameworks for instanceof checks.
|
|
1402
|
-
|
|
1403
|
-
};
|
|
1402
|
+
exposeCustomGlobal("Blob", class BlobStub {
|
|
1403
|
+
});
|
|
1404
1404
|
}
|
|
1405
1405
|
export default {
|
|
1406
1406
|
fetch,
|
package/dist/bridge/process.js
CHANGED
|
@@ -85,14 +85,47 @@ export class ProcessExitError extends Error {
|
|
|
85
85
|
}
|
|
86
86
|
// Make available globally
|
|
87
87
|
exposeCustomGlobal("ProcessExitError", ProcessExitError);
|
|
88
|
+
// Signal name → number mapping (POSIX standard)
|
|
89
|
+
const _signalNumbers = {
|
|
90
|
+
SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6,
|
|
91
|
+
SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12,
|
|
92
|
+
SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18,
|
|
93
|
+
SIGSTOP: 19, SIGTSTP: 20, SIGTTIN: 21, SIGTTOU: 22, SIGURG: 23,
|
|
94
|
+
SIGXCPU: 24, SIGXFSZ: 25, SIGVTALRM: 26, SIGPROF: 27, SIGWINCH: 28,
|
|
95
|
+
SIGIO: 29, SIGPWR: 30, SIGSYS: 31,
|
|
96
|
+
};
|
|
97
|
+
function _resolveSignal(signal) {
|
|
98
|
+
if (signal === undefined || signal === null)
|
|
99
|
+
return 15; // default SIGTERM
|
|
100
|
+
if (typeof signal === "number")
|
|
101
|
+
return signal;
|
|
102
|
+
const num = _signalNumbers[signal];
|
|
103
|
+
if (num !== undefined)
|
|
104
|
+
return num;
|
|
105
|
+
throw new Error("Unknown signal: " + signal);
|
|
106
|
+
}
|
|
88
107
|
const _processListeners = {};
|
|
89
108
|
const _processOnceListeners = {};
|
|
109
|
+
let _processMaxListeners = 10;
|
|
110
|
+
const _processMaxListenersWarned = new Set();
|
|
90
111
|
function _addListener(event, listener, once = false) {
|
|
91
112
|
const target = once ? _processOnceListeners : _processListeners;
|
|
92
113
|
if (!target[event]) {
|
|
93
114
|
target[event] = [];
|
|
94
115
|
}
|
|
95
116
|
target[event].push(listener);
|
|
117
|
+
// Warn when exceeding maxListeners (Node.js behavior: warn, don't crash)
|
|
118
|
+
if (_processMaxListeners > 0 && !_processMaxListenersWarned.has(event)) {
|
|
119
|
+
const total = (_processListeners[event]?.length ?? 0) + (_processOnceListeners[event]?.length ?? 0);
|
|
120
|
+
if (total > _processMaxListeners) {
|
|
121
|
+
_processMaxListenersWarned.add(event);
|
|
122
|
+
const warning = `MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${total} ${event} listeners added to [process]. MaxListeners is ${_processMaxListeners}. Use emitter.setMaxListeners() to increase limit`;
|
|
123
|
+
// Use console.error to emit warning without recursion risk
|
|
124
|
+
if (typeof _error !== "undefined") {
|
|
125
|
+
_error.applySync(undefined, [warning]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
96
129
|
return process;
|
|
97
130
|
}
|
|
98
131
|
function _removeListener(event, listener) {
|
|
@@ -385,6 +418,28 @@ const process = {
|
|
|
385
418
|
return _cwd;
|
|
386
419
|
},
|
|
387
420
|
chdir(dir) {
|
|
421
|
+
// Validate directory exists in VFS before setting cwd
|
|
422
|
+
let statJson;
|
|
423
|
+
try {
|
|
424
|
+
statJson = _fs.stat.applySyncPromise(undefined, [dir]);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
const err = new Error(`ENOENT: no such file or directory, chdir '${dir}'`);
|
|
428
|
+
err.code = "ENOENT";
|
|
429
|
+
err.errno = -2;
|
|
430
|
+
err.syscall = "chdir";
|
|
431
|
+
err.path = dir;
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
const parsed = JSON.parse(statJson);
|
|
435
|
+
if (!parsed.isDirectory) {
|
|
436
|
+
const err = new Error(`ENOTDIR: not a directory, chdir '${dir}'`);
|
|
437
|
+
err.code = "ENOTDIR";
|
|
438
|
+
err.errno = -20;
|
|
439
|
+
err.syscall = "chdir";
|
|
440
|
+
err.path = dir;
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
388
443
|
_cwd = dir;
|
|
389
444
|
},
|
|
390
445
|
get exitCode() {
|
|
@@ -499,11 +554,10 @@ const process = {
|
|
|
499
554
|
err.syscall = "kill";
|
|
500
555
|
throw err;
|
|
501
556
|
}
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
return true;
|
|
557
|
+
// Resolve signal name to number (default SIGTERM)
|
|
558
|
+
const sigNum = _resolveSignal(signal);
|
|
559
|
+
// Self-kill - exit with 128 + signal number (POSIX convention)
|
|
560
|
+
return process.exit(128 + sigNum);
|
|
507
561
|
},
|
|
508
562
|
// EventEmitter methods
|
|
509
563
|
on(event, listener) {
|
|
@@ -566,11 +620,12 @@ const process = {
|
|
|
566
620
|
]),
|
|
567
621
|
];
|
|
568
622
|
},
|
|
569
|
-
setMaxListeners() {
|
|
623
|
+
setMaxListeners(n) {
|
|
624
|
+
_processMaxListeners = n;
|
|
570
625
|
return process;
|
|
571
626
|
},
|
|
572
627
|
getMaxListeners() {
|
|
573
|
-
return
|
|
628
|
+
return _processMaxListeners;
|
|
574
629
|
},
|
|
575
630
|
rawListeners(event) {
|
|
576
631
|
return process.listeners(event);
|
|
@@ -588,33 +643,11 @@ const process = {
|
|
|
588
643
|
const msg = typeof warning === "string" ? warning : warning.message;
|
|
589
644
|
_emit("warning", { message: msg, name: "Warning" });
|
|
590
645
|
},
|
|
591
|
-
binding(
|
|
592
|
-
|
|
593
|
-
const stubs = {
|
|
594
|
-
fs: {},
|
|
595
|
-
buffer: {
|
|
596
|
-
Buffer: globalThis.Buffer,
|
|
597
|
-
constants: BUFFER_CONSTANTS,
|
|
598
|
-
kMaxLength: BUFFER_MAX_LENGTH,
|
|
599
|
-
kStringMaxLength: BUFFER_MAX_STRING_LENGTH,
|
|
600
|
-
},
|
|
601
|
-
process_wrap: {},
|
|
602
|
-
natives: {},
|
|
603
|
-
config: {},
|
|
604
|
-
uv: { UV_UDP_REUSEADDR: 4 },
|
|
605
|
-
constants: {
|
|
606
|
-
MAX_LENGTH: BUFFER_MAX_LENGTH,
|
|
607
|
-
MAX_STRING_LENGTH: BUFFER_MAX_STRING_LENGTH,
|
|
608
|
-
buffer: BUFFER_CONSTANTS,
|
|
609
|
-
},
|
|
610
|
-
crypto: {},
|
|
611
|
-
string_decoder: {},
|
|
612
|
-
os: {},
|
|
613
|
-
};
|
|
614
|
-
return stubs[name] || {};
|
|
646
|
+
binding(_name) {
|
|
647
|
+
throw new Error("process.binding is not supported in sandbox");
|
|
615
648
|
},
|
|
616
|
-
_linkedBinding(
|
|
617
|
-
|
|
649
|
+
_linkedBinding(_name) {
|
|
650
|
+
throw new Error("process._linkedBinding is not supported in sandbox");
|
|
618
651
|
},
|
|
619
652
|
dlopen() {
|
|
620
653
|
throw new Error("process.dlopen is not supported");
|
|
@@ -757,7 +790,8 @@ export function setInterval(callback, delay, ...args) {
|
|
|
757
790
|
const id = ++_timerId;
|
|
758
791
|
const handle = new TimerHandle(id);
|
|
759
792
|
_intervals.set(id, handle);
|
|
760
|
-
|
|
793
|
+
// Enforce minimum 1ms delay to prevent microtask CPU spin
|
|
794
|
+
const actualDelay = Math.max(1, delay ?? 0);
|
|
761
795
|
// Schedule interval execution
|
|
762
796
|
const scheduleNext = () => {
|
|
763
797
|
if (!_intervals.has(id))
|
|
@@ -831,6 +865,10 @@ export const cryptoPolyfill = {
|
|
|
831
865
|
if (typeof _cryptoRandomFill === "undefined") {
|
|
832
866
|
throwUnsupportedCryptoApi("getRandomValues");
|
|
833
867
|
}
|
|
868
|
+
// Web Crypto API spec caps getRandomValues at 65536 bytes.
|
|
869
|
+
if (array.byteLength > 65536) {
|
|
870
|
+
throw new RangeError(`The ArrayBufferView's byte length (${array.byteLength}) exceeds the number of bytes of entropy available via this API (65536)`);
|
|
871
|
+
}
|
|
834
872
|
const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
835
873
|
try {
|
|
836
874
|
const base64 = _cryptoRandomFill.applySync(undefined, [bytes.byteLength]);
|