@secure-exec/wasmvm 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.
Files changed (70) hide show
  1. package/LICENSE +191 -0
  2. package/dist/browser-driver.d.ts +68 -0
  3. package/dist/browser-driver.d.ts.map +1 -0
  4. package/dist/browser-driver.js +293 -0
  5. package/dist/browser-driver.js.map +1 -0
  6. package/dist/driver.d.ts +43 -0
  7. package/dist/driver.d.ts.map +1 -0
  8. package/dist/driver.js +1420 -0
  9. package/dist/driver.js.map +1 -0
  10. package/dist/fd-table.d.ts +67 -0
  11. package/dist/fd-table.d.ts.map +1 -0
  12. package/dist/fd-table.js +171 -0
  13. package/dist/fd-table.js.map +1 -0
  14. package/dist/index.d.ts +26 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +19 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/kernel-worker.d.ts +14 -0
  19. package/dist/kernel-worker.d.ts.map +1 -0
  20. package/dist/kernel-worker.js +1205 -0
  21. package/dist/kernel-worker.js.map +1 -0
  22. package/dist/module-cache.d.ts +21 -0
  23. package/dist/module-cache.d.ts.map +1 -0
  24. package/dist/module-cache.js +51 -0
  25. package/dist/module-cache.js.map +1 -0
  26. package/dist/permission-check.d.ts +36 -0
  27. package/dist/permission-check.d.ts.map +1 -0
  28. package/dist/permission-check.js +84 -0
  29. package/dist/permission-check.js.map +1 -0
  30. package/dist/ring-buffer.d.ts +64 -0
  31. package/dist/ring-buffer.d.ts.map +1 -0
  32. package/dist/ring-buffer.js +160 -0
  33. package/dist/ring-buffer.js.map +1 -0
  34. package/dist/syscall-rpc.d.ts +95 -0
  35. package/dist/syscall-rpc.d.ts.map +1 -0
  36. package/dist/syscall-rpc.js +48 -0
  37. package/dist/syscall-rpc.js.map +1 -0
  38. package/dist/user.d.ts +58 -0
  39. package/dist/user.d.ts.map +1 -0
  40. package/dist/user.js +143 -0
  41. package/dist/user.js.map +1 -0
  42. package/dist/wasi-constants.d.ts +77 -0
  43. package/dist/wasi-constants.d.ts.map +1 -0
  44. package/dist/wasi-constants.js +122 -0
  45. package/dist/wasi-constants.js.map +1 -0
  46. package/dist/wasi-file-io.d.ts +50 -0
  47. package/dist/wasi-file-io.d.ts.map +1 -0
  48. package/dist/wasi-file-io.js +10 -0
  49. package/dist/wasi-file-io.js.map +1 -0
  50. package/dist/wasi-polyfill.d.ts +368 -0
  51. package/dist/wasi-polyfill.d.ts.map +1 -0
  52. package/dist/wasi-polyfill.js +1438 -0
  53. package/dist/wasi-polyfill.js.map +1 -0
  54. package/dist/wasi-process-io.d.ts +35 -0
  55. package/dist/wasi-process-io.d.ts.map +1 -0
  56. package/dist/wasi-process-io.js +11 -0
  57. package/dist/wasi-process-io.js.map +1 -0
  58. package/dist/wasi-types.d.ts +175 -0
  59. package/dist/wasi-types.d.ts.map +1 -0
  60. package/dist/wasi-types.js +68 -0
  61. package/dist/wasi-types.js.map +1 -0
  62. package/dist/wasm-magic.d.ts +12 -0
  63. package/dist/wasm-magic.d.ts.map +1 -0
  64. package/dist/wasm-magic.js +53 -0
  65. package/dist/wasm-magic.js.map +1 -0
  66. package/dist/worker-adapter.d.ts +32 -0
  67. package/dist/worker-adapter.d.ts.map +1 -0
  68. package/dist/worker-adapter.js +147 -0
  69. package/dist/worker-adapter.js.map +1 -0
  70. package/package.json +35 -0
@@ -0,0 +1,1205 @@
1
+ /**
2
+ * Worker entry for WasmVM kernel-integrated execution.
3
+ *
4
+ * Runs a single WASM command inside a worker thread. Communicates
5
+ * with the main thread via SharedArrayBuffer RPC for synchronous
6
+ * kernel calls (file I/O, VFS, process spawn) and postMessage for
7
+ * stdout/stderr streaming.
8
+ *
9
+ * proc_spawn is provided as a host_process import so brush-shell
10
+ * pipeline stages route through KernelInterface.spawn() to the
11
+ * correct runtime driver.
12
+ */
13
+ import { workerData, parentPort } from 'node:worker_threads';
14
+ import { readFile } from 'node:fs/promises';
15
+ import { WasiPolyfill, WasiProcExit } from './wasi-polyfill.js';
16
+ import { UserManager } from './user.js';
17
+ import { FDTable } from './fd-table.js';
18
+ import { FILETYPE_CHARACTER_DEVICE, FILETYPE_REGULAR_FILE, FILETYPE_DIRECTORY, ERRNO_SUCCESS, ERRNO_EACCES, ERRNO_ECHILD, ERRNO_EINVAL, ERRNO_EBADF, } from './wasi-constants.js';
19
+ import { VfsError } from './wasi-types.js';
20
+ import { SIG_IDX_STATE, SIG_IDX_ERRNO, SIG_IDX_INT_RESULT, SIG_IDX_DATA_LEN, SIG_IDX_PENDING_SIGNAL, SIG_STATE_IDLE, RPC_WAIT_TIMEOUT_MS, } from './syscall-rpc.js';
21
+ import { isWriteBlocked as _isWriteBlocked, isSpawnBlocked as _isSpawnBlocked, isNetworkBlocked as _isNetworkBlocked, isPathInCwd as _isPathInCwd, validatePermissionTier, } from './permission-check.js';
22
+ import { normalize } from 'node:path';
23
+ const port = parentPort;
24
+ const init = workerData;
25
+ // Permission tier — validate to default unknown strings to 'isolated'
26
+ const permissionTier = validatePermissionTier(init.permissionTier ?? 'read-write');
27
+ /** Check if the tier blocks write operations. */
28
+ function isWriteBlocked() {
29
+ return _isWriteBlocked(permissionTier);
30
+ }
31
+ /** Check if the tier blocks subprocess spawning. */
32
+ function isSpawnBlocked() {
33
+ return _isSpawnBlocked(permissionTier);
34
+ }
35
+ /** Check if the tier blocks network operations. */
36
+ function isNetworkBlocked() {
37
+ return _isNetworkBlocked(permissionTier);
38
+ }
39
+ /**
40
+ * Resolve symlinks in path via VFS readlink RPC.
41
+ * Walks each path component and follows symlinks to prevent escape attacks.
42
+ */
43
+ function vfsRealpath(inputPath) {
44
+ const segments = inputPath.split('/').filter(Boolean);
45
+ const resolved = [];
46
+ let depth = 0;
47
+ const MAX_SYMLINK_DEPTH = 40; // POSIX SYMLOOP_MAX
48
+ for (let i = 0; i < segments.length; i++) {
49
+ resolved.push(segments[i]);
50
+ const currentPath = '/' + resolved.join('/');
51
+ // Try readlink directly via RPC (bypasses permission check)
52
+ const res = rpcCall('vfsReadlink', { path: currentPath });
53
+ if (res.errno === 0 && res.data.length > 0) {
54
+ if (++depth > MAX_SYMLINK_DEPTH)
55
+ return inputPath; // give up
56
+ const target = new TextDecoder().decode(res.data);
57
+ if (target.startsWith('/')) {
58
+ // Absolute symlink — restart from target
59
+ resolved.length = 0;
60
+ resolved.push(...target.split('/').filter(Boolean));
61
+ }
62
+ else {
63
+ // Relative symlink — replace last component with target
64
+ resolved.pop();
65
+ resolved.push(...target.split('/').filter(Boolean));
66
+ }
67
+ // Normalize away . and .. segments
68
+ const norm = normalize('/' + resolved.join('/')).split('/').filter(Boolean);
69
+ resolved.length = 0;
70
+ resolved.push(...norm);
71
+ }
72
+ }
73
+ return '/' + resolved.join('/') || '/';
74
+ }
75
+ /** Check if a path is within the cwd subtree (for isolated tier). */
76
+ function isPathInCwd(path) {
77
+ return _isPathInCwd(path, init.cwd, vfsRealpath);
78
+ }
79
+ // -------------------------------------------------------------------------
80
+ // RPC client — blocks worker thread until main thread responds
81
+ // -------------------------------------------------------------------------
82
+ const signalArr = new Int32Array(init.signalBuf);
83
+ const dataArr = new Uint8Array(init.dataBuf);
84
+ // Module-level reference for cooperative signal delivery — set after WASM instantiation
85
+ let wasmTrampoline = null;
86
+ function rpcCall(call, args) {
87
+ // Reset signal
88
+ Atomics.store(signalArr, SIG_IDX_STATE, SIG_STATE_IDLE);
89
+ // Post request
90
+ const msg = { type: 'syscall', call, args };
91
+ port.postMessage(msg);
92
+ // Block until response
93
+ while (true) {
94
+ const result = Atomics.wait(signalArr, SIG_IDX_STATE, SIG_STATE_IDLE, RPC_WAIT_TIMEOUT_MS);
95
+ if (result !== 'timed-out') {
96
+ break;
97
+ }
98
+ // poll(-1) can legally block forever, so keep waiting instead of turning
99
+ // the worker RPC guard timeout into a spurious EIO.
100
+ if (call === 'netPoll' && typeof args.timeout === 'number' && args.timeout < 0) {
101
+ continue;
102
+ }
103
+ return { errno: 76 /* EIO */, intResult: 0, data: new Uint8Array(0) };
104
+ }
105
+ // Read response
106
+ const errno = Atomics.load(signalArr, SIG_IDX_ERRNO);
107
+ const intResult = Atomics.load(signalArr, SIG_IDX_INT_RESULT);
108
+ const dataLen = Atomics.load(signalArr, SIG_IDX_DATA_LEN);
109
+ const data = dataLen > 0 ? dataArr.slice(0, dataLen) : new Uint8Array(0);
110
+ // Cooperative signal delivery — check piggybacked pending signal from driver
111
+ const pendingSig = Atomics.load(signalArr, SIG_IDX_PENDING_SIGNAL);
112
+ if (pendingSig !== 0 && wasmTrampoline) {
113
+ wasmTrampoline(pendingSig);
114
+ }
115
+ // Reset for next call
116
+ Atomics.store(signalArr, SIG_IDX_STATE, SIG_STATE_IDLE);
117
+ return { errno, intResult, data };
118
+ }
119
+ // -------------------------------------------------------------------------
120
+ // Local FD table — mirrors kernel state for rights checking / routing
121
+ // -------------------------------------------------------------------------
122
+ // Local FD → kernel FD mapping: the local FD table has a preopen at FD 3
123
+ // that the kernel doesn't know about, so opened-file FDs diverge.
124
+ const localToKernelFd = new Map();
125
+ /** Translate a worker-local FD to the kernel FD/socket ID it represents. */
126
+ function getKernelFd(localFd) {
127
+ return localToKernelFd.get(localFd) ?? localFd;
128
+ }
129
+ // Mapping-aware FDTable: updates localToKernelFd on renumber so pipe/redirect
130
+ // FDs remain reachable after WASI fd_renumber moves them to stdio positions.
131
+ // Also closes the kernel FD of the overwritten target (POSIX renumber semantics).
132
+ class KernelFDTable extends FDTable {
133
+ renumber(oldFd, newFd) {
134
+ if (oldFd === newFd) {
135
+ return this.has(oldFd) ? ERRNO_SUCCESS : ERRNO_EBADF;
136
+ }
137
+ // Capture mappings before super changes entries
138
+ const sourceMapping = localToKernelFd.get(oldFd);
139
+ const targetKernelFd = localToKernelFd.get(newFd) ?? newFd;
140
+ const result = super.renumber(oldFd, newFd);
141
+ if (result === ERRNO_SUCCESS) {
142
+ // Close kernel FD of overwritten target (mirrors POSIX close-on-renumber)
143
+ rpcCall('fdClose', { fd: targetKernelFd });
144
+ // Move source mapping to target position
145
+ localToKernelFd.delete(oldFd);
146
+ localToKernelFd.delete(newFd);
147
+ if (sourceMapping !== undefined) {
148
+ localToKernelFd.set(newFd, sourceMapping);
149
+ }
150
+ }
151
+ return result;
152
+ }
153
+ }
154
+ const fdTable = new KernelFDTable();
155
+ // -------------------------------------------------------------------------
156
+ // Kernel-backed WasiFileIO
157
+ // -------------------------------------------------------------------------
158
+ function createKernelFileIO() {
159
+ return {
160
+ fdRead(fd, maxBytes) {
161
+ const res = rpcCall('fdRead', { fd: getKernelFd(fd), length: maxBytes });
162
+ // Sync local cursor so fd_tell returns consistent values
163
+ if (res.errno === 0 && res.data.length > 0) {
164
+ const entry = fdTable.get(fd);
165
+ if (entry)
166
+ entry.cursor += BigInt(res.data.length);
167
+ }
168
+ return { errno: res.errno, data: res.data };
169
+ },
170
+ fdWrite(fd, data) {
171
+ // Permission check: read-only/isolated tiers can only write to stdout/stderr
172
+ if (isWriteBlocked() && fd !== 1 && fd !== 2) {
173
+ return { errno: ERRNO_EACCES, written: 0 };
174
+ }
175
+ const res = rpcCall('fdWrite', { fd: getKernelFd(fd), data: Array.from(data) });
176
+ // Sync local cursor so fd_tell returns consistent values
177
+ if (res.errno === 0 && res.intResult > 0) {
178
+ const entry = fdTable.get(fd);
179
+ if (entry)
180
+ entry.cursor += BigInt(res.intResult);
181
+ }
182
+ return { errno: res.errno, written: res.intResult };
183
+ },
184
+ fdOpen(path, dirflags, oflags, fdflags, rightsBase, rightsInheriting) {
185
+ const wantDirectory = !!(oflags & 0x2); // OFLAG_DIRECTORY
186
+ // Permission check: isolated tier restricts reads to cwd subtree
187
+ if (permissionTier === 'isolated' && !isPathInCwd(path)) {
188
+ return { errno: ERRNO_EACCES, fd: -1, filetype: 0 };
189
+ }
190
+ // Permission check: block write flags for read-only/isolated tiers
191
+ const hasWriteIntent = !!(oflags & 0x1) || !!(oflags & 0x8) || !!(fdflags & 0x1) || !!(rightsBase & 2n);
192
+ if (isWriteBlocked() && hasWriteIntent) {
193
+ return { errno: ERRNO_EACCES, fd: -1, filetype: 0 };
194
+ }
195
+ // Check if the path is actually a directory — some wasi-libc versions
196
+ // omit O_DIRECTORY in oflags when opening directories (e.g., nftw's
197
+ // opendir uses path_open with oflags=0). POSIX allows open(dir, O_RDONLY).
198
+ let isDirectory = wantDirectory;
199
+ if (!isDirectory) {
200
+ const probe = rpcCall('vfsStat', { path });
201
+ if (probe.errno === 0) {
202
+ const raw = JSON.parse(new TextDecoder().decode(probe.data));
203
+ if (raw.type === 'dir')
204
+ isDirectory = true;
205
+ }
206
+ }
207
+ // Directory opens: verify path exists as directory, return local FD
208
+ // No kernel FD needed — directory ops use VFS RPCs, not kernel fdRead
209
+ if (isDirectory) {
210
+ if (!wantDirectory) {
211
+ // Already stat'd above
212
+ }
213
+ else {
214
+ const statRes = rpcCall('vfsStat', { path });
215
+ if (statRes.errno !== 0)
216
+ return { errno: 44 /* ENOENT */, fd: -1, filetype: 0 };
217
+ }
218
+ const localFd = fdTable.open({ type: 'preopen', path }, { filetype: FILETYPE_DIRECTORY, rightsBase, rightsInheriting, fdflags, path });
219
+ return { errno: 0, fd: localFd, filetype: FILETYPE_DIRECTORY };
220
+ }
221
+ // Map WASI oflags to POSIX open flags for kernel
222
+ let flags = 0;
223
+ if (oflags & 0x1)
224
+ flags |= 0o100; // O_CREAT
225
+ if (oflags & 0x4)
226
+ flags |= 0o200; // O_EXCL
227
+ if (oflags & 0x8)
228
+ flags |= 0o1000; // O_TRUNC
229
+ if (fdflags & 0x1)
230
+ flags |= 0o2000; // O_APPEND
231
+ if (rightsBase & 2n)
232
+ flags |= 1; // O_WRONLY
233
+ const res = rpcCall('fdOpen', { path, flags, mode: 0o666 });
234
+ if (res.errno !== 0)
235
+ return { errno: res.errno, fd: -1, filetype: 0 };
236
+ const kFd = res.intResult; // kernel FD
237
+ // Mirror in local FDTable for polyfill rights checking
238
+ const localFd = fdTable.open({ type: 'vfsFile', ino: 0, path }, { filetype: FILETYPE_REGULAR_FILE, rightsBase, rightsInheriting, fdflags, path });
239
+ localToKernelFd.set(localFd, kFd);
240
+ return { errno: 0, fd: localFd, filetype: FILETYPE_REGULAR_FILE };
241
+ },
242
+ fdSeek(fd, offset, whence) {
243
+ const res = rpcCall('fdSeek', { fd: getKernelFd(fd), offset: offset.toString(), whence });
244
+ return { errno: res.errno, newOffset: BigInt(res.intResult) };
245
+ },
246
+ fdClose(fd) {
247
+ const entry = fdTable.get(fd);
248
+ const kFd = getKernelFd(fd);
249
+ fdTable.close(fd);
250
+ localToKernelFd.delete(fd);
251
+ const res = entry?.resource.type === 'socket'
252
+ ? rpcCall('netClose', { fd: kFd })
253
+ : rpcCall('fdClose', { fd: kFd });
254
+ return res.errno;
255
+ },
256
+ fdPread(fd, maxBytes, offset) {
257
+ const res = rpcCall('fdPread', { fd: getKernelFd(fd), length: maxBytes, offset: offset.toString() });
258
+ return { errno: res.errno, data: res.data };
259
+ },
260
+ fdPwrite(fd, data, offset) {
261
+ // Permission check: read-only/isolated tiers can only write to stdout/stderr
262
+ if (isWriteBlocked() && fd !== 1 && fd !== 2) {
263
+ return { errno: ERRNO_EACCES, written: 0 };
264
+ }
265
+ const res = rpcCall('fdPwrite', { fd: getKernelFd(fd), data: Array.from(data), offset: offset.toString() });
266
+ return { errno: res.errno, written: res.intResult };
267
+ },
268
+ };
269
+ }
270
+ // -------------------------------------------------------------------------
271
+ // Kernel-backed WasiProcessIO
272
+ // -------------------------------------------------------------------------
273
+ function createKernelProcessIO() {
274
+ return {
275
+ getArgs() {
276
+ return [init.command, ...init.args];
277
+ },
278
+ getEnviron() {
279
+ return init.env;
280
+ },
281
+ fdFdstatGet(fd) {
282
+ const entry = fdTable.get(fd);
283
+ if (!entry) {
284
+ return { errno: 8 /* EBADF */, filetype: 0, fdflags: 0, rightsBase: 0n, rightsInheriting: 0n };
285
+ }
286
+ return {
287
+ errno: 0,
288
+ filetype: entry.filetype,
289
+ fdflags: entry.fdflags,
290
+ rightsBase: entry.rightsBase,
291
+ rightsInheriting: entry.rightsInheriting,
292
+ };
293
+ },
294
+ procExit(exitCode) {
295
+ // Exit notification handled by WasiProcExit exception path
296
+ },
297
+ };
298
+ }
299
+ // -------------------------------------------------------------------------
300
+ // Kernel-backed VFS proxy — routes through RPC
301
+ // -------------------------------------------------------------------------
302
+ function createKernelVfs() {
303
+ const decoder = new TextDecoder();
304
+ // Inode cache for getIno/getInodeByIno — synthesizes inodes from kernel VFS stat
305
+ let nextIno = 1;
306
+ const pathToIno = new Map();
307
+ const inoToPath = new Map();
308
+ const inoCache = new Map();
309
+ const populatedDirs = new Set();
310
+ function resolveIno(path, followSymlinks = true) {
311
+ if (permissionTier === 'isolated' && !isPathInCwd(path))
312
+ return null;
313
+ // When following symlinks, use cached inode if available
314
+ if (followSymlinks) {
315
+ const cached = pathToIno.get(path);
316
+ if (cached !== undefined)
317
+ return cached;
318
+ }
319
+ const rpcName = followSymlinks ? 'vfsStat' : 'vfsLstat';
320
+ const res = rpcCall(rpcName, { path });
321
+ if (res.errno !== 0)
322
+ return null;
323
+ // RPC response fields: { type, mode, uid, gid, nlink, size, atime, mtime, ctime }
324
+ const raw = JSON.parse(decoder.decode(res.data));
325
+ const ino = nextIno++;
326
+ pathToIno.set(path, ino);
327
+ inoToPath.set(ino, path);
328
+ const nodeType = raw.type ?? 'file';
329
+ const isDir = nodeType === 'dir';
330
+ const node = {
331
+ type: nodeType,
332
+ mode: raw.mode ?? (isDir ? 0o40755 : 0o100644),
333
+ uid: raw.uid ?? 0,
334
+ gid: raw.gid ?? 0,
335
+ nlink: raw.nlink ?? 1,
336
+ size: raw.size ?? 0,
337
+ atime: raw.atime ?? Date.now(),
338
+ mtime: raw.mtime ?? Date.now(),
339
+ ctime: raw.ctime ?? Date.now(),
340
+ };
341
+ if (isDir) {
342
+ node.entries = new Map();
343
+ }
344
+ inoCache.set(ino, node);
345
+ return ino;
346
+ }
347
+ /** Lazy-populate directory entries from kernel VFS readdir. */
348
+ function populateDirEntries(ino, node) {
349
+ if (populatedDirs.has(ino))
350
+ return;
351
+ populatedDirs.add(ino);
352
+ const path = inoToPath.get(ino);
353
+ if (!path)
354
+ return;
355
+ // Isolated tier: skip populating directories outside cwd
356
+ if (permissionTier === 'isolated' && !isPathInCwd(path))
357
+ return;
358
+ const res = rpcCall('vfsReaddir', { path });
359
+ if (res.errno !== 0)
360
+ return;
361
+ const names = JSON.parse(decoder.decode(res.data));
362
+ for (const name of names) {
363
+ const childPath = path === '/' ? '/' + name : path + '/' + name;
364
+ const childIno = resolveIno(childPath);
365
+ if (childIno !== null) {
366
+ node.entries.set(name, childIno);
367
+ }
368
+ }
369
+ }
370
+ return {
371
+ exists(path) {
372
+ if (permissionTier === 'isolated' && !isPathInCwd(path))
373
+ return false;
374
+ const res = rpcCall('vfsExists', { path });
375
+ return res.errno === 0 && res.intResult === 1;
376
+ },
377
+ mkdir(path) {
378
+ if (isWriteBlocked())
379
+ throw new VfsError('EACCES', path);
380
+ const res = rpcCall('vfsMkdir', { path });
381
+ if (res.errno !== 0)
382
+ throw new VfsError('EACCES', path);
383
+ },
384
+ mkdirp(path) {
385
+ if (isWriteBlocked())
386
+ throw new VfsError('EACCES', path);
387
+ const segments = path.split('/').filter(Boolean);
388
+ let current = '';
389
+ for (const seg of segments) {
390
+ current += '/' + seg;
391
+ const exists = rpcCall('vfsExists', { path: current });
392
+ if (exists.errno === 0 && exists.intResult === 0) {
393
+ rpcCall('vfsMkdir', { path: current });
394
+ }
395
+ }
396
+ },
397
+ writeFile(path, data) {
398
+ if (isWriteBlocked())
399
+ throw new VfsError('EACCES', path);
400
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
401
+ rpcCall('vfsWriteFile', { path, data: Array.from(bytes) });
402
+ },
403
+ readFile(path) {
404
+ // Isolated tier: restrict reads to cwd subtree
405
+ if (permissionTier === 'isolated' && !isPathInCwd(path)) {
406
+ throw new VfsError('EACCES', path);
407
+ }
408
+ const res = rpcCall('vfsReadFile', { path });
409
+ if (res.errno !== 0)
410
+ throw new VfsError('ENOENT', path);
411
+ return res.data;
412
+ },
413
+ readdir(path) {
414
+ if (permissionTier === 'isolated' && !isPathInCwd(path)) {
415
+ throw new VfsError('EACCES', path);
416
+ }
417
+ const res = rpcCall('vfsReaddir', { path });
418
+ if (res.errno !== 0)
419
+ throw new VfsError('ENOENT', path);
420
+ return JSON.parse(decoder.decode(res.data));
421
+ },
422
+ stat(path) {
423
+ if (permissionTier === 'isolated' && !isPathInCwd(path)) {
424
+ throw new VfsError('EACCES', path);
425
+ }
426
+ const res = rpcCall('vfsStat', { path });
427
+ if (res.errno !== 0)
428
+ throw new VfsError('ENOENT', path);
429
+ return JSON.parse(decoder.decode(res.data));
430
+ },
431
+ lstat(path) {
432
+ if (permissionTier === 'isolated' && !isPathInCwd(path)) {
433
+ throw new VfsError('EACCES', path);
434
+ }
435
+ const res = rpcCall('vfsLstat', { path });
436
+ if (res.errno !== 0)
437
+ throw new VfsError('ENOENT', path);
438
+ return JSON.parse(decoder.decode(res.data));
439
+ },
440
+ unlink(path) {
441
+ if (isWriteBlocked())
442
+ throw new VfsError('EACCES', path);
443
+ const res = rpcCall('vfsUnlink', { path });
444
+ if (res.errno !== 0)
445
+ throw new VfsError('ENOENT', path);
446
+ },
447
+ rmdir(path) {
448
+ if (isWriteBlocked())
449
+ throw new VfsError('EACCES', path);
450
+ const res = rpcCall('vfsRmdir', { path });
451
+ if (res.errno !== 0)
452
+ throw new VfsError('ENOENT', path);
453
+ },
454
+ rename(oldPath, newPath) {
455
+ if (isWriteBlocked())
456
+ throw new VfsError('EACCES', oldPath);
457
+ const res = rpcCall('vfsRename', { oldPath, newPath });
458
+ if (res.errno !== 0)
459
+ throw new VfsError('ENOENT', oldPath);
460
+ },
461
+ symlink(target, linkPath) {
462
+ if (isWriteBlocked())
463
+ throw new VfsError('EACCES', linkPath);
464
+ const res = rpcCall('vfsSymlink', { target, linkPath });
465
+ if (res.errno !== 0)
466
+ throw new VfsError('EEXIST', linkPath);
467
+ },
468
+ readlink(path) {
469
+ if (permissionTier === 'isolated' && !isPathInCwd(path)) {
470
+ throw new VfsError('EACCES', path);
471
+ }
472
+ const res = rpcCall('vfsReadlink', { path });
473
+ if (res.errno !== 0)
474
+ throw new VfsError('EINVAL', path);
475
+ return decoder.decode(res.data);
476
+ },
477
+ chmod(_path, _mode) {
478
+ // No-op — permissions handled by kernel
479
+ },
480
+ getIno(path, followSymlinks = true) {
481
+ return resolveIno(path, followSymlinks);
482
+ },
483
+ getInodeByIno(ino) {
484
+ const node = inoCache.get(ino);
485
+ if (!node)
486
+ return null;
487
+ // Lazy-populate directory entries from kernel VFS
488
+ if (node.type === 'dir' && node.entries) {
489
+ populateDirEntries(ino, node);
490
+ }
491
+ return node;
492
+ },
493
+ snapshot() {
494
+ return [];
495
+ },
496
+ };
497
+ }
498
+ // -------------------------------------------------------------------------
499
+ // Host process imports — proc_spawn, fd_pipe, proc_kill route through kernel
500
+ // -------------------------------------------------------------------------
501
+ function createHostProcessImports(getMemory) {
502
+ // Track child PIDs for waitpid(-1) — "wait for any child"
503
+ const childPids = new Set();
504
+ return {
505
+ /**
506
+ * proc_spawn routes through KernelInterface.spawn() so brush-shell
507
+ * pipeline stages dispatch to the correct runtime driver.
508
+ *
509
+ * Matches Rust FFI: proc_spawn(argv_ptr, argv_len, envp_ptr, envp_len,
510
+ * stdin_fd, stdout_fd, stderr_fd, cwd_ptr, cwd_len, ret_pid) -> errno
511
+ */
512
+ proc_spawn(argv_ptr, argv_len, envp_ptr, envp_len, stdin_fd, stdout_fd, stderr_fd, cwd_ptr, cwd_len, ret_pid_ptr) {
513
+ // Permission check: only 'full' tier allows subprocess spawning
514
+ if (isSpawnBlocked())
515
+ return ERRNO_EACCES;
516
+ const mem = getMemory();
517
+ if (!mem)
518
+ return ERRNO_EINVAL;
519
+ const bytes = new Uint8Array(mem.buffer);
520
+ const decoder = new TextDecoder();
521
+ // Parse null-separated argv buffer — first entry is the command
522
+ const argvRaw = decoder.decode(bytes.slice(argv_ptr, argv_ptr + argv_len));
523
+ const argvParts = argvRaw.split('\0').filter(Boolean);
524
+ const command = argvParts[0] ?? '';
525
+ const args = argvParts.slice(1);
526
+ // Parse null-separated envp buffer (KEY=VALUE\0 pairs)
527
+ const env = {};
528
+ if (envp_len > 0) {
529
+ const envpRaw = decoder.decode(bytes.slice(envp_ptr, envp_ptr + envp_len));
530
+ for (const entry of envpRaw.split('\0')) {
531
+ if (!entry)
532
+ continue;
533
+ const eq = entry.indexOf('=');
534
+ if (eq > 0)
535
+ env[entry.slice(0, eq)] = entry.slice(eq + 1);
536
+ }
537
+ }
538
+ // Parse cwd
539
+ const cwd = cwd_len > 0
540
+ ? decoder.decode(bytes.slice(cwd_ptr, cwd_ptr + cwd_len))
541
+ : init.cwd;
542
+ // Convert local FDs to kernel FDs for pipe wiring
543
+ const stdinFd = stdin_fd === -1 ? undefined : (localToKernelFd.get(stdin_fd) ?? stdin_fd);
544
+ const stdoutFd = stdout_fd === -1 ? undefined : (localToKernelFd.get(stdout_fd) ?? stdout_fd);
545
+ const stderrFd = stderr_fd === -1 ? undefined : (localToKernelFd.get(stderr_fd) ?? stderr_fd);
546
+ // Route through kernel with FD overrides for pipe wiring
547
+ const res = rpcCall('spawn', {
548
+ command,
549
+ spawnArgs: args,
550
+ env,
551
+ cwd,
552
+ stdinFd,
553
+ stdoutFd,
554
+ stderrFd,
555
+ });
556
+ if (res.errno !== 0)
557
+ return res.errno;
558
+ const childPid = res.intResult;
559
+ new DataView(mem.buffer).setUint32(ret_pid_ptr, childPid, true);
560
+ childPids.add(childPid);
561
+ // Close pipe FDs used as stdio overrides in the parent (POSIX close-after-fork)
562
+ // Without this, the parent retains a reference to the pipe ends, preventing EOF.
563
+ for (const localFd of [stdin_fd, stdout_fd, stderr_fd]) {
564
+ if (localFd >= 0 && localToKernelFd.has(localFd)) {
565
+ const kFd = localToKernelFd.get(localFd);
566
+ fdTable.close(localFd);
567
+ localToKernelFd.delete(localFd);
568
+ rpcCall('fdClose', { fd: kFd });
569
+ }
570
+ }
571
+ return ERRNO_SUCCESS;
572
+ },
573
+ /**
574
+ * proc_waitpid(pid, options, ret_status, ret_pid) -> errno
575
+ * options: 0 = blocking, 1 = WNOHANG
576
+ * ret_pid: writes the actual waited-for PID (relevant for pid=-1)
577
+ */
578
+ proc_waitpid(pid, options, ret_status_ptr, ret_pid_ptr) {
579
+ const mem = getMemory();
580
+ if (!mem)
581
+ return ERRNO_EINVAL;
582
+ // Resolve pid=-1 (wait for any child) to an actual child PID
583
+ let targetPid = pid;
584
+ if (pid < 0) {
585
+ const first = childPids.values().next();
586
+ if (first.done)
587
+ return ERRNO_ECHILD;
588
+ targetPid = first.value;
589
+ }
590
+ const res = rpcCall('waitpid', { pid: targetPid, options: options || undefined });
591
+ if (res.errno !== 0)
592
+ return res.errno;
593
+ // WNOHANG returns intResult=-1 when process is still running
594
+ if (res.intResult === -1) {
595
+ const view = new DataView(mem.buffer);
596
+ view.setUint32(ret_status_ptr, 0, true);
597
+ view.setUint32(ret_pid_ptr, 0, true);
598
+ return ERRNO_SUCCESS;
599
+ }
600
+ const view = new DataView(mem.buffer);
601
+ view.setUint32(ret_status_ptr, res.intResult, true);
602
+ view.setUint32(ret_pid_ptr, targetPid, true);
603
+ // Remove from tracked children after successful wait
604
+ childPids.delete(targetPid);
605
+ return ERRNO_SUCCESS;
606
+ },
607
+ /** proc_kill(pid, signal) -> errno — only 'full' tier can send signals */
608
+ proc_kill(pid, signal) {
609
+ if (isSpawnBlocked())
610
+ return ERRNO_EACCES;
611
+ const res = rpcCall('kill', { pid, signal });
612
+ return res.errno;
613
+ },
614
+ /**
615
+ * fd_pipe(ret_read_fd, ret_write_fd) -> errno
616
+ * Creates a kernel pipe and installs both ends in this process's FD table.
617
+ * Registers pipe FDs in the local FDTable so WASI fd_renumber can find them.
618
+ */
619
+ fd_pipe(ret_read_fd_ptr, ret_write_fd_ptr) {
620
+ // Permission check: pipes are only useful with proc_spawn, restrict to 'full' tier
621
+ if (isSpawnBlocked())
622
+ return ERRNO_EACCES;
623
+ const mem = getMemory();
624
+ if (!mem)
625
+ return ERRNO_EINVAL;
626
+ const res = rpcCall('pipe', {});
627
+ if (res.errno !== 0)
628
+ return res.errno;
629
+ // Read/write FDs packed in intResult: read in low 16 bits, write in high 16 bits
630
+ const kernelReadFd = res.intResult & 0xFFFF;
631
+ const kernelWriteFd = (res.intResult >>> 16) & 0xFFFF;
632
+ // Register pipe FDs in local table as vfsFile — fd_read/fd_write for
633
+ // vfsFile routes through kernel FileIO bridge, which detects pipe FDs
634
+ const localReadFd = fdTable.open({ type: 'vfsFile', ino: 0, path: '' }, { filetype: FILETYPE_CHARACTER_DEVICE });
635
+ const localWriteFd = fdTable.open({ type: 'vfsFile', ino: 0, path: '' }, { filetype: FILETYPE_CHARACTER_DEVICE });
636
+ localToKernelFd.set(localReadFd, kernelReadFd);
637
+ localToKernelFd.set(localWriteFd, kernelWriteFd);
638
+ const view = new DataView(mem.buffer);
639
+ view.setUint32(ret_read_fd_ptr, localReadFd, true);
640
+ view.setUint32(ret_write_fd_ptr, localWriteFd, true);
641
+ return ERRNO_SUCCESS;
642
+ },
643
+ /**
644
+ * fd_dup(fd, ret_new_fd) -> errno
645
+ * Converts local FD to kernel FD, dups in kernel, registers new local FD.
646
+ */
647
+ fd_dup(fd, ret_new_fd_ptr) {
648
+ // Permission check: prevent resource exhaustion from restricted tiers
649
+ if (isSpawnBlocked())
650
+ return ERRNO_EACCES;
651
+ const mem = getMemory();
652
+ if (!mem)
653
+ return ERRNO_EINVAL;
654
+ const kFd = localToKernelFd.get(fd) ?? fd;
655
+ const res = rpcCall('fdDup', { fd: kFd });
656
+ if (res.errno !== 0)
657
+ return res.errno;
658
+ const newKernelFd = res.intResult;
659
+ const newLocalFd = fdTable.open({ type: 'vfsFile', ino: 0, path: '' }, { filetype: FILETYPE_CHARACTER_DEVICE });
660
+ localToKernelFd.set(newLocalFd, newKernelFd);
661
+ new DataView(mem.buffer).setUint32(ret_new_fd_ptr, newLocalFd, true);
662
+ return ERRNO_SUCCESS;
663
+ },
664
+ /**
665
+ * fd_dup_min(fd, min_fd, ret_new_fd) -> errno
666
+ * F_DUPFD semantics: duplicate fd to lowest available FD >= min_fd.
667
+ */
668
+ fd_dup_min(fd, min_fd, ret_new_fd_ptr) {
669
+ if (isSpawnBlocked())
670
+ return ERRNO_EACCES;
671
+ const mem = getMemory();
672
+ if (!mem)
673
+ return ERRNO_EINVAL;
674
+ const kFd = localToKernelFd.get(fd) ?? fd;
675
+ const res = rpcCall('fdDupMin', { fd: kFd, minFd: min_fd });
676
+ if (res.errno !== 0)
677
+ return res.errno;
678
+ const newKernelFd = res.intResult;
679
+ const newLocalFd = fdTable.dupMinFd(fd, min_fd);
680
+ if (newLocalFd < 0)
681
+ return ERRNO_EBADF;
682
+ localToKernelFd.set(newLocalFd, newKernelFd);
683
+ new DataView(mem.buffer).setUint32(ret_new_fd_ptr, newLocalFd, true);
684
+ return ERRNO_SUCCESS;
685
+ },
686
+ /** proc_getpid(ret_pid) -> errno */
687
+ proc_getpid(ret_pid_ptr) {
688
+ const mem = getMemory();
689
+ if (!mem)
690
+ return ERRNO_EINVAL;
691
+ new DataView(mem.buffer).setUint32(ret_pid_ptr, init.pid, true);
692
+ return ERRNO_SUCCESS;
693
+ },
694
+ /** proc_getppid(ret_pid) -> errno */
695
+ proc_getppid(ret_pid_ptr) {
696
+ const mem = getMemory();
697
+ if (!mem)
698
+ return ERRNO_EINVAL;
699
+ new DataView(mem.buffer).setUint32(ret_pid_ptr, init.ppid, true);
700
+ return ERRNO_SUCCESS;
701
+ },
702
+ /**
703
+ * fd_dup2(old_fd, new_fd) -> errno
704
+ * Duplicates old_fd to new_fd. If new_fd is already open, it is closed first.
705
+ */
706
+ fd_dup2(old_fd, new_fd) {
707
+ // Permission check: prevent resource exhaustion from restricted tiers
708
+ if (isSpawnBlocked())
709
+ return ERRNO_EACCES;
710
+ const kOldFd = localToKernelFd.get(old_fd) ?? old_fd;
711
+ const kNewFd = localToKernelFd.get(new_fd) ?? new_fd;
712
+ const res = rpcCall('fdDup2', { oldFd: kOldFd, newFd: kNewFd });
713
+ if (res.errno !== 0)
714
+ return res.errno;
715
+ // Update local FD table to reflect the dup2
716
+ const errno = fdTable.dup2(old_fd, new_fd);
717
+ if (errno !== ERRNO_SUCCESS)
718
+ return errno;
719
+ // Map local new_fd to kNewFd (the kernel fd it now owns after dup2).
720
+ // Using kNewFd (not kOldFd) preserves independent ownership: closing
721
+ // new_fd closes kNewFd without affecting old_fd's kOldFd.
722
+ localToKernelFd.set(new_fd, kNewFd);
723
+ return ERRNO_SUCCESS;
724
+ },
725
+ /** sleep_ms(milliseconds) -> errno — blocks via Atomics.wait */
726
+ sleep_ms(milliseconds) {
727
+ const buf = new Int32Array(new SharedArrayBuffer(4));
728
+ Atomics.wait(buf, 0, 0, milliseconds);
729
+ return ERRNO_SUCCESS;
730
+ },
731
+ /**
732
+ * pty_open(ret_master_fd, ret_write_fd) -> errno
733
+ * Allocates a PTY master/slave pair via the kernel and installs both FDs.
734
+ * The slave FD is passed to proc_spawn as stdin/stdout/stderr for interactive use.
735
+ */
736
+ pty_open(ret_master_fd_ptr, ret_slave_fd_ptr) {
737
+ if (isSpawnBlocked())
738
+ return ERRNO_EACCES;
739
+ const mem = getMemory();
740
+ if (!mem)
741
+ return ERRNO_EINVAL;
742
+ const res = rpcCall('openpty', {});
743
+ if (res.errno !== 0)
744
+ return res.errno;
745
+ // Master + slave kernel FDs packed: low 16 bits = masterFd, high 16 bits = slaveFd
746
+ const kernelMasterFd = res.intResult & 0xFFFF;
747
+ const kernelSlaveFd = (res.intResult >>> 16) & 0xFFFF;
748
+ // Register PTY FDs in local table (same pattern as fd_pipe)
749
+ const localMasterFd = fdTable.open({ type: 'vfsFile', ino: 0, path: '' }, { filetype: FILETYPE_CHARACTER_DEVICE });
750
+ const localSlaveFd = fdTable.open({ type: 'vfsFile', ino: 0, path: '' }, { filetype: FILETYPE_CHARACTER_DEVICE });
751
+ localToKernelFd.set(localMasterFd, kernelMasterFd);
752
+ localToKernelFd.set(localSlaveFd, kernelSlaveFd);
753
+ const view = new DataView(mem.buffer);
754
+ view.setUint32(ret_master_fd_ptr, localMasterFd, true);
755
+ view.setUint32(ret_slave_fd_ptr, localSlaveFd, true);
756
+ return ERRNO_SUCCESS;
757
+ },
758
+ /**
759
+ * proc_sigaction(signal, action) -> errno
760
+ * Register signal disposition: 0=SIG_DFL, 1=SIG_IGN, 2=user handler.
761
+ * For action=2, the C sysroot holds the function pointer; the kernel
762
+ * only needs to know the signal should be caught (cooperative delivery).
763
+ */
764
+ proc_sigaction(signal, action) {
765
+ if (signal < 1 || signal > 64)
766
+ return ERRNO_EINVAL;
767
+ const res = rpcCall('sigaction', { signal, action });
768
+ return res.errno;
769
+ },
770
+ };
771
+ }
772
+ // -------------------------------------------------------------------------
773
+ // Host net imports — TCP socket operations routed through the kernel
774
+ // -------------------------------------------------------------------------
775
+ function createHostNetImports(getMemory) {
776
+ function openLocalSocketFd(kernelSocketId) {
777
+ const localFd = fdTable.open({ type: 'socket', kernelId: kernelSocketId }, { filetype: FILETYPE_CHARACTER_DEVICE });
778
+ localToKernelFd.set(localFd, kernelSocketId);
779
+ return localFd;
780
+ }
781
+ return {
782
+ /** net_socket(domain, type, protocol, ret_fd) -> errno */
783
+ net_socket(domain, type, protocol, ret_fd_ptr) {
784
+ if (isNetworkBlocked())
785
+ return ERRNO_EACCES;
786
+ const mem = getMemory();
787
+ if (!mem)
788
+ return ERRNO_EINVAL;
789
+ const res = rpcCall('netSocket', { domain, type, protocol });
790
+ if (res.errno !== 0)
791
+ return res.errno;
792
+ const localFd = openLocalSocketFd(res.intResult);
793
+ new DataView(mem.buffer).setUint32(ret_fd_ptr, localFd, true);
794
+ return ERRNO_SUCCESS;
795
+ },
796
+ /** net_connect(fd, addr_ptr, addr_len) -> errno */
797
+ net_connect(fd, addr_ptr, addr_len) {
798
+ if (isNetworkBlocked())
799
+ return ERRNO_EACCES;
800
+ const mem = getMemory();
801
+ if (!mem)
802
+ return ERRNO_EINVAL;
803
+ const addrBytes = new Uint8Array(mem.buffer, addr_ptr, addr_len);
804
+ const addr = new TextDecoder().decode(addrBytes);
805
+ const res = rpcCall('netConnect', { fd: getKernelFd(fd), addr });
806
+ return res.errno;
807
+ },
808
+ /** net_send(fd, buf_ptr, buf_len, flags, ret_sent) -> errno */
809
+ net_send(fd, buf_ptr, buf_len, flags, ret_sent_ptr) {
810
+ if (isNetworkBlocked())
811
+ return ERRNO_EACCES;
812
+ const mem = getMemory();
813
+ if (!mem)
814
+ return ERRNO_EINVAL;
815
+ const sendData = new Uint8Array(mem.buffer).slice(buf_ptr, buf_ptr + buf_len);
816
+ const res = rpcCall('netSend', { fd: getKernelFd(fd), data: Array.from(sendData), flags });
817
+ if (res.errno !== 0)
818
+ return res.errno;
819
+ new DataView(mem.buffer).setUint32(ret_sent_ptr, res.intResult, true);
820
+ return ERRNO_SUCCESS;
821
+ },
822
+ /** net_recv(fd, buf_ptr, buf_len, flags, ret_received) -> errno */
823
+ net_recv(fd, buf_ptr, buf_len, flags, ret_received_ptr) {
824
+ if (isNetworkBlocked())
825
+ return ERRNO_EACCES;
826
+ const mem = getMemory();
827
+ if (!mem)
828
+ return ERRNO_EINVAL;
829
+ const res = rpcCall('netRecv', { fd: getKernelFd(fd), length: buf_len, flags });
830
+ if (res.errno !== 0)
831
+ return res.errno;
832
+ // Copy received data into WASM memory
833
+ const dest = new Uint8Array(mem.buffer, buf_ptr, buf_len);
834
+ dest.set(res.data.subarray(0, Math.min(res.data.length, buf_len)));
835
+ new DataView(mem.buffer).setUint32(ret_received_ptr, res.data.length, true);
836
+ return ERRNO_SUCCESS;
837
+ },
838
+ /** net_close(fd) -> errno */
839
+ net_close(fd) {
840
+ if (isNetworkBlocked())
841
+ return ERRNO_EACCES;
842
+ const res = rpcCall('netClose', { fd: getKernelFd(fd) });
843
+ if (res.errno === 0) {
844
+ localToKernelFd.delete(fd);
845
+ }
846
+ return res.errno;
847
+ },
848
+ /** net_tls_connect(fd, hostname_ptr, hostname_len, flags?) -> errno
849
+ * flags: 0 = verify peer (default), 1 = skip verification (-k) */
850
+ net_tls_connect(fd, hostname_ptr, hostname_len, flags) {
851
+ if (isNetworkBlocked())
852
+ return ERRNO_EACCES;
853
+ const mem = getMemory();
854
+ if (!mem)
855
+ return ERRNO_EINVAL;
856
+ const hostnameBytes = new Uint8Array(mem.buffer, hostname_ptr, hostname_len);
857
+ const hostname = new TextDecoder().decode(hostnameBytes);
858
+ const verifyPeer = (flags ?? 0) === 0;
859
+ const res = rpcCall('netTlsConnect', { fd: getKernelFd(fd), hostname, verifyPeer });
860
+ return res.errno;
861
+ },
862
+ /** net_getaddrinfo(host_ptr, host_len, port_ptr, port_len, ret_addr, ret_addr_len) -> errno */
863
+ net_getaddrinfo(host_ptr, host_len, port_ptr, port_len, ret_addr_ptr, ret_addr_len_ptr) {
864
+ if (isNetworkBlocked())
865
+ return ERRNO_EACCES;
866
+ const mem = getMemory();
867
+ if (!mem)
868
+ return ERRNO_EINVAL;
869
+ const decoder = new TextDecoder();
870
+ const host = decoder.decode(new Uint8Array(mem.buffer, host_ptr, host_len));
871
+ const port = decoder.decode(new Uint8Array(mem.buffer, port_ptr, port_len));
872
+ const res = rpcCall('netGetaddrinfo', { host, port });
873
+ if (res.errno !== 0)
874
+ return res.errno;
875
+ // Write resolved address data back to WASM memory
876
+ const maxLen = new DataView(mem.buffer).getUint32(ret_addr_len_ptr, true);
877
+ const dataLen = res.data.length;
878
+ if (dataLen > maxLen)
879
+ return ERRNO_EINVAL;
880
+ const wasmBuf = new Uint8Array(mem.buffer);
881
+ wasmBuf.set(res.data.subarray(0, dataLen), ret_addr_ptr);
882
+ new DataView(mem.buffer).setUint32(ret_addr_len_ptr, dataLen, true);
883
+ return 0;
884
+ },
885
+ /** net_setsockopt(fd, level, optname, optval_ptr, optval_len) -> errno */
886
+ net_setsockopt(fd, level, optname, optval_ptr, optval_len) {
887
+ if (isNetworkBlocked())
888
+ return ERRNO_EACCES;
889
+ const mem = getMemory();
890
+ if (!mem)
891
+ return ERRNO_EINVAL;
892
+ const optval = new Uint8Array(mem.buffer).slice(optval_ptr, optval_ptr + optval_len);
893
+ const res = rpcCall('netSetsockopt', {
894
+ fd: getKernelFd(fd),
895
+ level,
896
+ optname,
897
+ optval: Array.from(optval),
898
+ });
899
+ return res.errno;
900
+ },
901
+ /** net_getsockopt(fd, level, optname, optval_ptr, optval_len_ptr) -> errno */
902
+ net_getsockopt(fd, level, optname, optval_ptr, optval_len_ptr) {
903
+ if (isNetworkBlocked())
904
+ return ERRNO_EACCES;
905
+ const mem = getMemory();
906
+ if (!mem)
907
+ return ERRNO_EINVAL;
908
+ const view = new DataView(mem.buffer);
909
+ const optvalLen = view.getUint32(optval_len_ptr, true);
910
+ const res = rpcCall('netGetsockopt', {
911
+ fd: getKernelFd(fd),
912
+ level,
913
+ optname,
914
+ optvalLen,
915
+ });
916
+ if (res.errno !== 0)
917
+ return res.errno;
918
+ if (res.data.length > optvalLen)
919
+ return ERRNO_EINVAL;
920
+ const wasmBuf = new Uint8Array(mem.buffer);
921
+ wasmBuf.set(res.data, optval_ptr);
922
+ view.setUint32(optval_len_ptr, res.data.length, true);
923
+ return ERRNO_SUCCESS;
924
+ },
925
+ /** net_getsockname(fd, ret_addr, ret_addr_len) -> errno */
926
+ net_getsockname(fd, ret_addr_ptr, ret_addr_len_ptr) {
927
+ if (isNetworkBlocked())
928
+ return ERRNO_EACCES;
929
+ const mem = getMemory();
930
+ if (!mem)
931
+ return ERRNO_EINVAL;
932
+ const view = new DataView(mem.buffer);
933
+ const maxAddrLen = view.getUint32(ret_addr_len_ptr, true);
934
+ const res = rpcCall('kernelSocketGetLocalAddr', { fd: getKernelFd(fd) });
935
+ if (res.errno !== 0)
936
+ return res.errno;
937
+ if (res.data.length > maxAddrLen)
938
+ return ERRNO_EINVAL;
939
+ const wasmBuf = new Uint8Array(mem.buffer);
940
+ wasmBuf.set(res.data, ret_addr_ptr);
941
+ view.setUint32(ret_addr_len_ptr, res.data.length, true);
942
+ return ERRNO_SUCCESS;
943
+ },
944
+ /** net_getpeername(fd, ret_addr, ret_addr_len) -> errno */
945
+ net_getpeername(fd, ret_addr_ptr, ret_addr_len_ptr) {
946
+ if (isNetworkBlocked())
947
+ return ERRNO_EACCES;
948
+ const mem = getMemory();
949
+ if (!mem)
950
+ return ERRNO_EINVAL;
951
+ const view = new DataView(mem.buffer);
952
+ const maxAddrLen = view.getUint32(ret_addr_len_ptr, true);
953
+ const res = rpcCall('kernelSocketGetRemoteAddr', { fd: getKernelFd(fd) });
954
+ if (res.errno !== 0)
955
+ return res.errno;
956
+ if (res.data.length > maxAddrLen)
957
+ return ERRNO_EINVAL;
958
+ const wasmBuf = new Uint8Array(mem.buffer);
959
+ wasmBuf.set(res.data, ret_addr_ptr);
960
+ view.setUint32(ret_addr_len_ptr, res.data.length, true);
961
+ return ERRNO_SUCCESS;
962
+ },
963
+ /** net_bind(fd, addr_ptr, addr_len) -> errno */
964
+ net_bind(fd, addr_ptr, addr_len) {
965
+ if (isNetworkBlocked())
966
+ return ERRNO_EACCES;
967
+ const mem = getMemory();
968
+ if (!mem)
969
+ return ERRNO_EINVAL;
970
+ const addrBytes = new Uint8Array(mem.buffer, addr_ptr, addr_len);
971
+ const addr = new TextDecoder().decode(addrBytes);
972
+ const res = rpcCall('netBind', { fd: getKernelFd(fd), addr });
973
+ return res.errno;
974
+ },
975
+ /** net_listen(fd, backlog) -> errno */
976
+ net_listen(fd, backlog) {
977
+ if (isNetworkBlocked())
978
+ return ERRNO_EACCES;
979
+ const res = rpcCall('netListen', { fd: getKernelFd(fd), backlog });
980
+ return res.errno;
981
+ },
982
+ /** net_accept(fd, ret_fd, ret_addr, ret_addr_len) -> errno */
983
+ net_accept(fd, ret_fd_ptr, ret_addr_ptr, ret_addr_len_ptr) {
984
+ if (isNetworkBlocked())
985
+ return ERRNO_EACCES;
986
+ const mem = getMemory();
987
+ if (!mem)
988
+ return ERRNO_EINVAL;
989
+ const res = rpcCall('netAccept', { fd: getKernelFd(fd) });
990
+ if (res.errno !== 0)
991
+ return res.errno;
992
+ const view = new DataView(mem.buffer);
993
+ const newFd = openLocalSocketFd(res.intResult);
994
+ view.setUint32(ret_fd_ptr, newFd, true);
995
+ // res.data contains the remote address string as UTF-8 bytes
996
+ const maxAddrLen = view.getUint32(ret_addr_len_ptr, true);
997
+ const addrLen = Math.min(res.data.length, maxAddrLen);
998
+ const wasmBuf = new Uint8Array(mem.buffer);
999
+ wasmBuf.set(res.data.subarray(0, addrLen), ret_addr_ptr);
1000
+ view.setUint32(ret_addr_len_ptr, addrLen, true);
1001
+ return ERRNO_SUCCESS;
1002
+ },
1003
+ /** net_sendto(fd, buf_ptr, buf_len, flags, addr_ptr, addr_len, ret_sent) -> errno */
1004
+ net_sendto(fd, buf_ptr, buf_len, flags, addr_ptr, addr_len, ret_sent_ptr) {
1005
+ if (isNetworkBlocked())
1006
+ return ERRNO_EACCES;
1007
+ const mem = getMemory();
1008
+ if (!mem)
1009
+ return ERRNO_EINVAL;
1010
+ const sendData = new Uint8Array(mem.buffer).slice(buf_ptr, buf_ptr + buf_len);
1011
+ const addrBytes = new Uint8Array(mem.buffer, addr_ptr, addr_len);
1012
+ const addr = new TextDecoder().decode(addrBytes);
1013
+ const res = rpcCall('netSendTo', { fd: getKernelFd(fd), data: Array.from(sendData), flags, addr });
1014
+ if (res.errno !== 0)
1015
+ return res.errno;
1016
+ new DataView(mem.buffer).setUint32(ret_sent_ptr, res.intResult, true);
1017
+ return ERRNO_SUCCESS;
1018
+ },
1019
+ /** net_recvfrom(fd, buf_ptr, buf_len, flags, ret_received, ret_addr, ret_addr_len) -> errno */
1020
+ net_recvfrom(fd, buf_ptr, buf_len, flags, ret_received_ptr, ret_addr_ptr, ret_addr_len_ptr) {
1021
+ if (isNetworkBlocked())
1022
+ return ERRNO_EACCES;
1023
+ const mem = getMemory();
1024
+ if (!mem)
1025
+ return ERRNO_EINVAL;
1026
+ const res = rpcCall('netRecvFrom', { fd: getKernelFd(fd), length: buf_len, flags });
1027
+ if (res.errno !== 0)
1028
+ return res.errno;
1029
+ // intResult = received data length; data buffer = [data | addr bytes]
1030
+ const dataLen = res.intResult;
1031
+ const dest = new Uint8Array(mem.buffer, buf_ptr, buf_len);
1032
+ dest.set(res.data.subarray(0, Math.min(dataLen, buf_len)));
1033
+ new DataView(mem.buffer).setUint32(ret_received_ptr, dataLen, true);
1034
+ // Source address bytes follow data in the buffer
1035
+ const view = new DataView(mem.buffer);
1036
+ const maxAddrLen = view.getUint32(ret_addr_len_ptr, true);
1037
+ const addrBytes = res.data.subarray(dataLen);
1038
+ const addrLen = Math.min(addrBytes.length, maxAddrLen);
1039
+ const wasmBuf = new Uint8Array(mem.buffer);
1040
+ wasmBuf.set(addrBytes.subarray(0, addrLen), ret_addr_ptr);
1041
+ view.setUint32(ret_addr_len_ptr, addrLen, true);
1042
+ return ERRNO_SUCCESS;
1043
+ },
1044
+ /** net_poll(fds_ptr, nfds, timeout_ms, ret_ready) -> errno */
1045
+ net_poll(fds_ptr, nfds, timeout_ms, ret_ready_ptr) {
1046
+ // No permission gate — poll() is a generic FD operation (pipes, files, sockets).
1047
+ const mem = getMemory();
1048
+ if (!mem)
1049
+ return ERRNO_EINVAL;
1050
+ // Read pollfd entries from WASM memory: each is 8 bytes (fd:i32, events:i16, revents:i16)
1051
+ // Translate local FDs to kernel FDs so the driver can look up pipes/sockets
1052
+ const view = new DataView(mem.buffer);
1053
+ const fds = [];
1054
+ for (let i = 0; i < nfds; i++) {
1055
+ const base = fds_ptr + i * 8;
1056
+ const localFd = view.getInt32(base, true);
1057
+ const events = view.getInt16(base + 4, true);
1058
+ fds.push({ fd: getKernelFd(localFd), events });
1059
+ }
1060
+ const res = rpcCall('netPoll', { fds, timeout: timeout_ms });
1061
+ if (res.errno !== 0)
1062
+ return res.errno;
1063
+ // Parse revents from response data (JSON array)
1064
+ const reventsJson = new TextDecoder().decode(res.data.subarray(0, res.data.length));
1065
+ const revents = JSON.parse(reventsJson);
1066
+ // Write revents back into WASM memory pollfd structs
1067
+ for (let i = 0; i < nfds && i < revents.length; i++) {
1068
+ const base = fds_ptr + i * 8;
1069
+ view.setInt16(base + 6, revents[i], true); // revents field offset = 6
1070
+ }
1071
+ view.setUint32(ret_ready_ptr, res.intResult, true);
1072
+ return ERRNO_SUCCESS;
1073
+ },
1074
+ };
1075
+ }
1076
+ // -------------------------------------------------------------------------
1077
+ // Main execution
1078
+ // -------------------------------------------------------------------------
1079
+ async function main() {
1080
+ let wasmMemory = null;
1081
+ const getMemory = () => wasmMemory;
1082
+ const fileIO = createKernelFileIO();
1083
+ const processIO = createKernelProcessIO();
1084
+ const vfs = createKernelVfs();
1085
+ const polyfill = new WasiPolyfill(fdTable, vfs, {
1086
+ fileIO,
1087
+ processIO,
1088
+ args: [init.command, ...init.args],
1089
+ env: init.env,
1090
+ });
1091
+ // Route stdin through kernel pipe when piped
1092
+ if (init.stdinFd !== undefined) {
1093
+ polyfill.setStdinReader((buf, offset, length) => {
1094
+ const res = rpcCall('fdRead', { fd: 0, length });
1095
+ if (res.errno !== 0 || res.data.length === 0)
1096
+ return 0; // EOF or error
1097
+ const n = Math.min(res.data.length, length);
1098
+ buf.set(res.data.subarray(0, n), offset);
1099
+ return n;
1100
+ });
1101
+ }
1102
+ // Stream stdout/stderr — route through kernel pipe when FD is overridden,
1103
+ // otherwise stream to main thread via postMessage
1104
+ if (init.stdoutFd !== undefined && init.stdoutFd !== 1) {
1105
+ // Stdout is piped — route writes through kernel fdWrite on FD 1
1106
+ polyfill.setStdoutWriter((buf, offset, length) => {
1107
+ const data = buf.slice(offset, offset + length);
1108
+ rpcCall('fdWrite', { fd: 1, data: Array.from(data) });
1109
+ return length;
1110
+ });
1111
+ }
1112
+ else {
1113
+ polyfill.setStdoutWriter((buf, offset, length) => {
1114
+ port.postMessage({ type: 'stdout', data: buf.slice(offset, offset + length) });
1115
+ return length;
1116
+ });
1117
+ }
1118
+ if (init.stderrFd !== undefined && init.stderrFd !== 2) {
1119
+ // Stderr is piped — route writes through kernel fdWrite on FD 2
1120
+ polyfill.setStderrWriter((buf, offset, length) => {
1121
+ const data = buf.slice(offset, offset + length);
1122
+ rpcCall('fdWrite', { fd: 2, data: Array.from(data) });
1123
+ return length;
1124
+ });
1125
+ }
1126
+ else {
1127
+ polyfill.setStderrWriter((buf, offset, length) => {
1128
+ port.postMessage({ type: 'stderr', data: buf.slice(offset, offset + length) });
1129
+ return length;
1130
+ });
1131
+ }
1132
+ const userManager = new UserManager({
1133
+ getMemory,
1134
+ fdTable,
1135
+ ttyFds: init.ttyFds ? new Set(init.ttyFds) : false,
1136
+ });
1137
+ // Check for pending signals while poll_oneoff sleeps inside the WASI polyfill.
1138
+ polyfill.setSleepHook(() => {
1139
+ rpcCall('getpid', { pid: init.pid });
1140
+ });
1141
+ const hostProcess = createHostProcessImports(getMemory);
1142
+ const hostNet = createHostNetImports(getMemory);
1143
+ try {
1144
+ // Use pre-compiled module from main thread if available, otherwise compile from disk
1145
+ const wasmModule = init.wasmModule
1146
+ ?? await WebAssembly.compile(await readFile(init.wasmBinaryPath));
1147
+ const imports = {
1148
+ wasi_snapshot_preview1: polyfill.getImports(),
1149
+ host_user: userManager.getImports(),
1150
+ host_process: hostProcess,
1151
+ host_net: hostNet,
1152
+ };
1153
+ const instance = await WebAssembly.instantiate(wasmModule, imports);
1154
+ wasmMemory = instance.exports.memory;
1155
+ polyfill.setMemory(wasmMemory);
1156
+ // Wire cooperative signal delivery trampoline (if the WASM binary exports it)
1157
+ const trampoline = instance.exports.__wasi_signal_trampoline;
1158
+ if (trampoline)
1159
+ wasmTrampoline = trampoline;
1160
+ // Run the command
1161
+ const start = instance.exports._start;
1162
+ start();
1163
+ // Normal exit — flush collected output, close piped FDs for EOF
1164
+ flushOutput(polyfill);
1165
+ closePipedFds();
1166
+ port.postMessage({ type: 'exit', code: 0 });
1167
+ }
1168
+ catch (err) {
1169
+ if (err instanceof WasiProcExit) {
1170
+ flushOutput(polyfill);
1171
+ closePipedFds();
1172
+ port.postMessage({ type: 'exit', code: err.exitCode });
1173
+ }
1174
+ else {
1175
+ const errMsg = err instanceof Error ? err.message : String(err);
1176
+ port.postMessage({ type: 'stderr', data: new TextEncoder().encode(errMsg + '\n') });
1177
+ closePipedFds();
1178
+ port.postMessage({ type: 'exit', code: 1 });
1179
+ }
1180
+ }
1181
+ }
1182
+ /** Close piped stdio FDs so readers get EOF. */
1183
+ function closePipedFds() {
1184
+ if (init.stdoutFd !== undefined && init.stdoutFd !== 1) {
1185
+ rpcCall('fdClose', { fd: 1 });
1186
+ }
1187
+ if (init.stderrFd !== undefined && init.stderrFd !== 2) {
1188
+ rpcCall('fdClose', { fd: 2 });
1189
+ }
1190
+ }
1191
+ /** Flush any remaining collected output (not caught by streaming writers). */
1192
+ function flushOutput(polyfill) {
1193
+ const stdout = polyfill.stdout;
1194
+ if (stdout.length > 0)
1195
+ port.postMessage({ type: 'stdout', data: stdout });
1196
+ const stderr = polyfill.stderr;
1197
+ if (stderr.length > 0)
1198
+ port.postMessage({ type: 'stderr', data: stderr });
1199
+ }
1200
+ main().catch((err) => {
1201
+ const errMsg = err instanceof Error ? err.message : String(err);
1202
+ port.postMessage({ type: 'stderr', data: new TextEncoder().encode(errMsg + '\n') });
1203
+ port.postMessage({ type: 'exit', code: 1 });
1204
+ });
1205
+ //# sourceMappingURL=kernel-worker.js.map