@secure-exec/core 0.1.0-rc.2 → 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.
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Register an active handle that keeps the sandbox alive.
3
+ * Throws if the handle cap (_maxHandles) would be exceeded.
3
4
  * @param id Unique identifier for the handle
4
5
  * @param description Human-readable description for debugging
5
6
  */
@@ -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
- pid = Math.floor(Math.random() * 10000) + 1000;
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 && !maxBufferExceeded) {
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 && !maxBufferExceeded) {
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: 0,
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: Math.floor(Math.random() * 10000) + 1000,
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: Math.floor(Math.random() * 10000) + 1000,
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: 0,
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
- // In our virtual fs, just normalize the path (keep /data prefix for consistency)
1988
- return toPathString(path)
1989
- .replace(/\/\/+/g, "/")
1990
- .replace(/\/$/, "") || "/";
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 toPathString(path)
1994
- .replace(/\/\/+/g, "/")
1995
- .replace(/\/$/, "") || "/";
2079
+ return fs.realpathSync(path);
1996
2080
  }
1997
2081
  }),
1998
2082
  realpath: Object.assign(function realpath(path, callback) {
@@ -1392,15 +1392,15 @@ exposeCustomGlobal("_httpsModule", https);
1392
1392
  exposeCustomGlobal("_http2Module", http2);
1393
1393
  exposeCustomGlobal("_dnsModule", dns);
1394
1394
  exposeCustomGlobal("_httpServerDispatch", dispatchServerRequest);
1395
- // Make fetch API available globally
1396
- globalThis.fetch = fetch;
1397
- globalThis.Headers = Headers;
1398
- globalThis.Request = Request;
1399
- globalThis.Response = Response;
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
- globalThis.Blob = class BlobStub {
1403
- };
1402
+ exposeCustomGlobal("Blob", class BlobStub {
1403
+ });
1404
1404
  }
1405
1405
  export default {
1406
1406
  fetch,
@@ -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
- // Self-kill - treat as exit
503
- if (!signal || signal === "SIGTERM" || signal === 15) {
504
- process.exit(143);
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 10;
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(name) {
592
- // Return stub implementations for common bindings
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(name) {
617
- return process.binding(name);
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
- const actualDelay = delay ?? 0;
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]);