@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.
- package/LICENSE +191 -0
- package/dist/browser-driver.d.ts +68 -0
- package/dist/browser-driver.d.ts.map +1 -0
- package/dist/browser-driver.js +293 -0
- package/dist/browser-driver.js.map +1 -0
- package/dist/driver.d.ts +43 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +1420 -0
- package/dist/driver.js.map +1 -0
- package/dist/fd-table.d.ts +67 -0
- package/dist/fd-table.d.ts.map +1 -0
- package/dist/fd-table.js +171 -0
- package/dist/fd-table.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel-worker.d.ts +14 -0
- package/dist/kernel-worker.d.ts.map +1 -0
- package/dist/kernel-worker.js +1205 -0
- package/dist/kernel-worker.js.map +1 -0
- package/dist/module-cache.d.ts +21 -0
- package/dist/module-cache.d.ts.map +1 -0
- package/dist/module-cache.js +51 -0
- package/dist/module-cache.js.map +1 -0
- package/dist/permission-check.d.ts +36 -0
- package/dist/permission-check.d.ts.map +1 -0
- package/dist/permission-check.js +84 -0
- package/dist/permission-check.js.map +1 -0
- package/dist/ring-buffer.d.ts +64 -0
- package/dist/ring-buffer.d.ts.map +1 -0
- package/dist/ring-buffer.js +160 -0
- package/dist/ring-buffer.js.map +1 -0
- package/dist/syscall-rpc.d.ts +95 -0
- package/dist/syscall-rpc.d.ts.map +1 -0
- package/dist/syscall-rpc.js +48 -0
- package/dist/syscall-rpc.js.map +1 -0
- package/dist/user.d.ts +58 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +143 -0
- package/dist/user.js.map +1 -0
- package/dist/wasi-constants.d.ts +77 -0
- package/dist/wasi-constants.d.ts.map +1 -0
- package/dist/wasi-constants.js +122 -0
- package/dist/wasi-constants.js.map +1 -0
- package/dist/wasi-file-io.d.ts +50 -0
- package/dist/wasi-file-io.d.ts.map +1 -0
- package/dist/wasi-file-io.js +10 -0
- package/dist/wasi-file-io.js.map +1 -0
- package/dist/wasi-polyfill.d.ts +368 -0
- package/dist/wasi-polyfill.d.ts.map +1 -0
- package/dist/wasi-polyfill.js +1438 -0
- package/dist/wasi-polyfill.js.map +1 -0
- package/dist/wasi-process-io.d.ts +35 -0
- package/dist/wasi-process-io.d.ts.map +1 -0
- package/dist/wasi-process-io.js +11 -0
- package/dist/wasi-process-io.js.map +1 -0
- package/dist/wasi-types.d.ts +175 -0
- package/dist/wasi-types.d.ts.map +1 -0
- package/dist/wasi-types.js +68 -0
- package/dist/wasi-types.js.map +1 -0
- package/dist/wasm-magic.d.ts +12 -0
- package/dist/wasm-magic.d.ts.map +1 -0
- package/dist/wasm-magic.js +53 -0
- package/dist/wasm-magic.js.map +1 -0
- package/dist/worker-adapter.d.ts +32 -0
- package/dist/worker-adapter.d.ts.map +1 -0
- package/dist/worker-adapter.js +147 -0
- package/dist/worker-adapter.js.map +1 -0
- package/package.json +35 -0
package/dist/driver.js
ADDED
|
@@ -0,0 +1,1420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WasmVM runtime driver for kernel integration.
|
|
3
|
+
*
|
|
4
|
+
* Discovers WASM command binaries from filesystem directories (commandDirs),
|
|
5
|
+
* validates them by WASM magic bytes, and loads them on demand. Each spawn()
|
|
6
|
+
* creates a Worker thread that loads the per-command binary and communicates
|
|
7
|
+
* with the main thread via SharedArrayBuffer-based RPC for synchronous
|
|
8
|
+
* WASI syscalls.
|
|
9
|
+
*
|
|
10
|
+
* proc_spawn from brush-shell routes through KernelInterface.spawn()
|
|
11
|
+
* so pipeline stages can dispatch to any runtime (WasmVM, Node, Python).
|
|
12
|
+
*/
|
|
13
|
+
import { AF_INET, AF_INET6, AF_UNIX, SOCK_STREAM, SOCK_DGRAM, resolveProcSelfPath, } from '@secure-exec/core';
|
|
14
|
+
import { WorkerAdapter } from './worker-adapter.js';
|
|
15
|
+
import { SIGNAL_BUFFER_BYTES, DATA_BUFFER_BYTES, RPC_WAIT_TIMEOUT_MS, SIG_IDX_STATE, SIG_IDX_ERRNO, SIG_IDX_INT_RESULT, SIG_IDX_DATA_LEN, SIG_IDX_PENDING_SIGNAL, SIG_STATE_READY, } from './syscall-rpc.js';
|
|
16
|
+
import { ERRNO_MAP, ERRNO_EIO } from './wasi-constants.js';
|
|
17
|
+
import { isWasmBinary, isWasmBinarySync } from './wasm-magic.js';
|
|
18
|
+
import { resolvePermissionTier } from './permission-check.js';
|
|
19
|
+
import { ModuleCache } from './module-cache.js';
|
|
20
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
21
|
+
import { existsSync, statSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { connect as tlsConnect } from 'node:tls';
|
|
24
|
+
import { lookup } from 'node:dns/promises';
|
|
25
|
+
// wasi-libc bottom-half socket constants differ from the kernel's POSIX-facing
|
|
26
|
+
// constants, so normalize them at the host_net boundary.
|
|
27
|
+
const WASI_AF_INET = 1;
|
|
28
|
+
const WASI_AF_INET6 = 2;
|
|
29
|
+
const WASI_AF_UNIX = 3;
|
|
30
|
+
const WASI_SOCK_DGRAM = 5;
|
|
31
|
+
const WASI_SOCK_STREAM = 6;
|
|
32
|
+
const WASI_SOCK_TYPE_FLAGS = 0x6000;
|
|
33
|
+
function normalizeSocketDomain(domain) {
|
|
34
|
+
switch (domain) {
|
|
35
|
+
case WASI_AF_INET:
|
|
36
|
+
return AF_INET;
|
|
37
|
+
case WASI_AF_INET6:
|
|
38
|
+
return AF_INET6;
|
|
39
|
+
case WASI_AF_UNIX:
|
|
40
|
+
return AF_UNIX;
|
|
41
|
+
default:
|
|
42
|
+
return domain;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function normalizeSocketType(type) {
|
|
46
|
+
switch (type & ~WASI_SOCK_TYPE_FLAGS) {
|
|
47
|
+
case WASI_SOCK_DGRAM:
|
|
48
|
+
return SOCK_DGRAM;
|
|
49
|
+
case WASI_SOCK_STREAM:
|
|
50
|
+
return SOCK_STREAM;
|
|
51
|
+
default:
|
|
52
|
+
return type & ~WASI_SOCK_TYPE_FLAGS;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function scopedProcPath(pid, path) {
|
|
56
|
+
return resolveProcSelfPath(path, pid);
|
|
57
|
+
}
|
|
58
|
+
function decodeSocketOptionValue(optval) {
|
|
59
|
+
if (optval.byteLength === 0 || optval.byteLength > 6) {
|
|
60
|
+
throw Object.assign(new Error('EINVAL: invalid socket option length'), { code: 'EINVAL' });
|
|
61
|
+
}
|
|
62
|
+
// Decode little-endian integers exactly as wasi-libc passes them to host_net.
|
|
63
|
+
let value = 0;
|
|
64
|
+
for (let index = 0; index < optval.byteLength; index++) {
|
|
65
|
+
value += optval[index] * (2 ** (index * 8));
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function encodeSocketOptionValue(value, byteLength) {
|
|
70
|
+
if (!Number.isInteger(byteLength) || byteLength <= 0 || byteLength > 6) {
|
|
71
|
+
throw Object.assign(new Error('EINVAL: invalid socket option length'), { code: 'EINVAL' });
|
|
72
|
+
}
|
|
73
|
+
const encoded = new Uint8Array(byteLength);
|
|
74
|
+
let remaining = value;
|
|
75
|
+
for (let index = 0; index < byteLength; index++) {
|
|
76
|
+
encoded[index] = remaining % 0x100;
|
|
77
|
+
remaining = Math.floor(remaining / 0x100);
|
|
78
|
+
}
|
|
79
|
+
return encoded;
|
|
80
|
+
}
|
|
81
|
+
function serializeSockAddr(addr) {
|
|
82
|
+
return 'host' in addr ? `${addr.host}:${addr.port}` : addr.path;
|
|
83
|
+
}
|
|
84
|
+
function getKernelWorkerUrl() {
|
|
85
|
+
const siblingWorkerUrl = new URL('./kernel-worker.js', import.meta.url);
|
|
86
|
+
if (existsSync(siblingWorkerUrl)) {
|
|
87
|
+
return siblingWorkerUrl;
|
|
88
|
+
}
|
|
89
|
+
return new URL('../dist/kernel-worker.js', import.meta.url);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* All commands available in the WasmVM runtime.
|
|
93
|
+
* Used as fallback when no commandDirs are configured (legacy mode).
|
|
94
|
+
* @deprecated Use commandDirs option instead — commands are discovered from filesystem.
|
|
95
|
+
*/
|
|
96
|
+
export const WASMVM_COMMANDS = [
|
|
97
|
+
// Shell
|
|
98
|
+
'sh', 'bash',
|
|
99
|
+
// Text processing
|
|
100
|
+
'grep', 'egrep', 'fgrep', 'rg', 'sed', 'awk', 'jq', 'yq',
|
|
101
|
+
// Find
|
|
102
|
+
'find', 'fd',
|
|
103
|
+
// Built-in implementations
|
|
104
|
+
'cat', 'chmod', 'column', 'cp', 'dd', 'diff', 'du', 'expr', 'file', 'head',
|
|
105
|
+
'ln', 'logname', 'ls', 'mkdir', 'mktemp', 'mv', 'pathchk', 'rev', 'rm',
|
|
106
|
+
'sleep', 'sort', 'split', 'stat', 'strings', 'tac', 'tail', 'test',
|
|
107
|
+
'[', 'touch', 'tree', 'tsort', 'whoami',
|
|
108
|
+
// Compression & Archiving
|
|
109
|
+
'gzip', 'gunzip', 'zcat', 'tar', 'zip', 'unzip',
|
|
110
|
+
// Data Processing (C programs)
|
|
111
|
+
'sqlite3',
|
|
112
|
+
// Network (C programs)
|
|
113
|
+
'curl', 'wget',
|
|
114
|
+
// Build tools (C programs)
|
|
115
|
+
'make',
|
|
116
|
+
// Version control (C programs)
|
|
117
|
+
'git', 'git-remote-http', 'git-remote-https',
|
|
118
|
+
// Shim commands
|
|
119
|
+
'env', 'envsubst', 'nice', 'nohup', 'stdbuf', 'timeout', 'xargs',
|
|
120
|
+
// uutils: text/encoding
|
|
121
|
+
'base32', 'base64', 'basenc', 'basename', 'comm', 'cut',
|
|
122
|
+
'dircolors', 'dirname', 'echo', 'expand', 'factor', 'false',
|
|
123
|
+
'fmt', 'fold', 'join', 'nl', 'numfmt', 'od', 'paste',
|
|
124
|
+
'printenv', 'printf', 'ptx', 'seq', 'shuf', 'tr', 'true',
|
|
125
|
+
'unexpand', 'uniq', 'wc', 'yes',
|
|
126
|
+
// uutils: checksums
|
|
127
|
+
'b2sum', 'cksum', 'md5sum', 'sha1sum', 'sha224sum', 'sha256sum',
|
|
128
|
+
'sha384sum', 'sha512sum', 'sum',
|
|
129
|
+
// uutils: file operations
|
|
130
|
+
'link', 'pwd', 'readlink', 'realpath', 'rmdir', 'shred', 'tee',
|
|
131
|
+
'truncate', 'unlink',
|
|
132
|
+
// uutils: system info
|
|
133
|
+
'arch', 'date', 'nproc', 'uname',
|
|
134
|
+
// uutils: ls variants
|
|
135
|
+
'dir', 'vdir',
|
|
136
|
+
// Stubbed commands
|
|
137
|
+
'hostname', 'hostid', 'more', 'sync', 'tty',
|
|
138
|
+
'chcon', 'runcon',
|
|
139
|
+
'chgrp', 'chown',
|
|
140
|
+
'chroot',
|
|
141
|
+
'df',
|
|
142
|
+
'groups', 'id',
|
|
143
|
+
'install',
|
|
144
|
+
'kill',
|
|
145
|
+
'mkfifo', 'mknod',
|
|
146
|
+
'pinky', 'who', 'users', 'uptime',
|
|
147
|
+
'stty',
|
|
148
|
+
// Codex CLI (host_process spawn via wasi-spawn)
|
|
149
|
+
'codex',
|
|
150
|
+
// Codex headless agent (non-TUI entry point)
|
|
151
|
+
'codex-exec',
|
|
152
|
+
// Internal test: WasiChild host_process spawn validation
|
|
153
|
+
'spawn-test-host',
|
|
154
|
+
// Internal test: wasi-http HTTP client validation via host_net
|
|
155
|
+
'http-test',
|
|
156
|
+
];
|
|
157
|
+
Object.freeze(WASMVM_COMMANDS);
|
|
158
|
+
/**
|
|
159
|
+
* Default permission tiers for known first-party commands.
|
|
160
|
+
* User-provided permissions override these defaults.
|
|
161
|
+
*/
|
|
162
|
+
export const DEFAULT_FIRST_PARTY_TIERS = {
|
|
163
|
+
// Shell — needs proc_spawn for pipelines and subcommands
|
|
164
|
+
'sh': 'full',
|
|
165
|
+
'bash': 'full',
|
|
166
|
+
// Shims — spawn child processes as their core function
|
|
167
|
+
'env': 'full',
|
|
168
|
+
'timeout': 'full',
|
|
169
|
+
'xargs': 'full',
|
|
170
|
+
'nice': 'full',
|
|
171
|
+
'nohup': 'full',
|
|
172
|
+
'stdbuf': 'full',
|
|
173
|
+
// Build tools — spawns child processes to run recipes
|
|
174
|
+
'make': 'full',
|
|
175
|
+
// Codex CLI — spawns child processes via wasi-spawn
|
|
176
|
+
'codex': 'full',
|
|
177
|
+
// Codex headless agent — spawns processes + uses network
|
|
178
|
+
'codex-exec': 'full',
|
|
179
|
+
// Internal test — exercises WasiChild host_process spawn
|
|
180
|
+
'spawn-test-host': 'full',
|
|
181
|
+
// Internal test — exercises wasi-http HTTP client via host_net
|
|
182
|
+
'http-test': 'full',
|
|
183
|
+
// Version control — reads/writes .git objects, remote operations use network
|
|
184
|
+
'git': 'full',
|
|
185
|
+
'git-remote-http': 'full',
|
|
186
|
+
'git-remote-https': 'full',
|
|
187
|
+
// Read-only tools — never need to write files
|
|
188
|
+
'grep': 'read-only',
|
|
189
|
+
'egrep': 'read-only',
|
|
190
|
+
'fgrep': 'read-only',
|
|
191
|
+
'rg': 'read-only',
|
|
192
|
+
'cat': 'read-only',
|
|
193
|
+
'head': 'read-only',
|
|
194
|
+
'tail': 'read-only',
|
|
195
|
+
'wc': 'read-only',
|
|
196
|
+
'sort': 'read-only',
|
|
197
|
+
'uniq': 'read-only',
|
|
198
|
+
'diff': 'read-only',
|
|
199
|
+
'find': 'read-only',
|
|
200
|
+
'fd': 'read-only',
|
|
201
|
+
'tree': 'read-only',
|
|
202
|
+
'file': 'read-only',
|
|
203
|
+
'du': 'read-only',
|
|
204
|
+
'ls': 'read-only',
|
|
205
|
+
'dir': 'read-only',
|
|
206
|
+
'vdir': 'read-only',
|
|
207
|
+
'strings': 'read-only',
|
|
208
|
+
'stat': 'read-only',
|
|
209
|
+
'rev': 'read-only',
|
|
210
|
+
'column': 'read-only',
|
|
211
|
+
'cut': 'read-only',
|
|
212
|
+
'tr': 'read-only',
|
|
213
|
+
'paste': 'read-only',
|
|
214
|
+
'join': 'read-only',
|
|
215
|
+
'fold': 'read-only',
|
|
216
|
+
'expand': 'read-only',
|
|
217
|
+
'nl': 'read-only',
|
|
218
|
+
'od': 'read-only',
|
|
219
|
+
'comm': 'read-only',
|
|
220
|
+
'basename': 'read-only',
|
|
221
|
+
'dirname': 'read-only',
|
|
222
|
+
'realpath': 'read-only',
|
|
223
|
+
'readlink': 'read-only',
|
|
224
|
+
'pwd': 'read-only',
|
|
225
|
+
'echo': 'read-only',
|
|
226
|
+
'envsubst': 'read-only',
|
|
227
|
+
'printf': 'read-only',
|
|
228
|
+
'true': 'read-only',
|
|
229
|
+
'false': 'read-only',
|
|
230
|
+
'yes': 'read-only',
|
|
231
|
+
'seq': 'read-only',
|
|
232
|
+
'test': 'read-only',
|
|
233
|
+
'[': 'read-only',
|
|
234
|
+
'expr': 'read-only',
|
|
235
|
+
'factor': 'read-only',
|
|
236
|
+
'date': 'read-only',
|
|
237
|
+
'uname': 'read-only',
|
|
238
|
+
'nproc': 'read-only',
|
|
239
|
+
'whoami': 'read-only',
|
|
240
|
+
'id': 'read-only',
|
|
241
|
+
'groups': 'read-only',
|
|
242
|
+
'base64': 'read-only',
|
|
243
|
+
'md5sum': 'read-only',
|
|
244
|
+
'sha256sum': 'read-only',
|
|
245
|
+
'tac': 'read-only',
|
|
246
|
+
'tsort': 'read-only',
|
|
247
|
+
// Network — needs socket access for HTTP, can write with -o/-O
|
|
248
|
+
'curl': 'full',
|
|
249
|
+
'wget': 'full',
|
|
250
|
+
// Data processing — need write for file-based databases
|
|
251
|
+
'sqlite3': 'read-write',
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Create a WasmVM RuntimeDriver that can be mounted into the kernel.
|
|
255
|
+
*/
|
|
256
|
+
export function createWasmVmRuntime(options) {
|
|
257
|
+
return new WasmVmRuntimeDriver(options);
|
|
258
|
+
}
|
|
259
|
+
class WasmVmRuntimeDriver {
|
|
260
|
+
name = 'wasmvm';
|
|
261
|
+
// Dynamic commands list — populated from filesystem scan or legacy WASMVM_COMMANDS
|
|
262
|
+
_commands = [];
|
|
263
|
+
// Command name → binary path map (commandDirs mode only)
|
|
264
|
+
_commandPaths = new Map();
|
|
265
|
+
_commandDirs;
|
|
266
|
+
// Legacy mode: single binary path
|
|
267
|
+
_wasmBinaryPath;
|
|
268
|
+
_legacyMode;
|
|
269
|
+
// Per-command permission tiers
|
|
270
|
+
_permissions;
|
|
271
|
+
_kernel = null;
|
|
272
|
+
_activeWorkers = new Map();
|
|
273
|
+
_workerAdapter = new WorkerAdapter();
|
|
274
|
+
_moduleCache = new ModuleCache();
|
|
275
|
+
// TLS-upgraded sockets bypass kernel recv — direct host TLS I/O
|
|
276
|
+
_tlsSockets = new Map();
|
|
277
|
+
// Per-PID queue of signals pending cooperative delivery to WASM trampoline
|
|
278
|
+
_wasmPendingSignals = new Map();
|
|
279
|
+
get commands() { return this._commands; }
|
|
280
|
+
constructor(options) {
|
|
281
|
+
this._commandDirs = options?.commandDirs ?? [];
|
|
282
|
+
this._wasmBinaryPath = options?.wasmBinaryPath ?? '';
|
|
283
|
+
this._permissions = options?.permissions ?? {};
|
|
284
|
+
// Legacy mode when wasmBinaryPath is set and commandDirs is not
|
|
285
|
+
this._legacyMode = !options?.commandDirs && !!options?.wasmBinaryPath;
|
|
286
|
+
if (this._legacyMode) {
|
|
287
|
+
// Deprecated path — use static command list
|
|
288
|
+
this._commands = [...WASMVM_COMMANDS];
|
|
289
|
+
}
|
|
290
|
+
// Emit deprecation warning for wasmBinaryPath
|
|
291
|
+
if (options?.wasmBinaryPath && options?.commandDirs) {
|
|
292
|
+
console.warn('WasmVmRuntime: wasmBinaryPath is deprecated and ignored when commandDirs is set. ' +
|
|
293
|
+
'Use commandDirs only.');
|
|
294
|
+
}
|
|
295
|
+
else if (options?.wasmBinaryPath) {
|
|
296
|
+
console.warn('WasmVmRuntime: wasmBinaryPath is deprecated. Use commandDirs instead.');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async init(kernel) {
|
|
300
|
+
this._kernel = kernel;
|
|
301
|
+
// Scan commandDirs for WASM binaries (skip in legacy mode)
|
|
302
|
+
if (!this._legacyMode && this._commandDirs.length > 0) {
|
|
303
|
+
await this._scanCommandDirs();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* On-demand discovery: synchronously check commandDirs for a binary.
|
|
308
|
+
* Called by the kernel when CommandRegistry.resolve() returns null.
|
|
309
|
+
*/
|
|
310
|
+
tryResolve(command) {
|
|
311
|
+
// Not applicable in legacy mode
|
|
312
|
+
if (this._legacyMode)
|
|
313
|
+
return false;
|
|
314
|
+
// Already known
|
|
315
|
+
if (this._commandPaths.has(command))
|
|
316
|
+
return true;
|
|
317
|
+
for (const dir of this._commandDirs) {
|
|
318
|
+
const fullPath = join(dir, command);
|
|
319
|
+
try {
|
|
320
|
+
if (!existsSync(fullPath))
|
|
321
|
+
continue;
|
|
322
|
+
// Skip directories
|
|
323
|
+
const st = statSync(fullPath);
|
|
324
|
+
if (st.isDirectory())
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
// Sync 4-byte WASM magic check
|
|
331
|
+
if (!isWasmBinarySync(fullPath))
|
|
332
|
+
continue;
|
|
333
|
+
this._commandPaths.set(command, fullPath);
|
|
334
|
+
if (!this._commands.includes(command))
|
|
335
|
+
this._commands.push(command);
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
spawn(command, args, ctx) {
|
|
341
|
+
const kernel = this._kernel;
|
|
342
|
+
if (!kernel)
|
|
343
|
+
throw new Error('WasmVM driver not initialized');
|
|
344
|
+
// Resolve binary path for this command
|
|
345
|
+
const binaryPath = this._resolveBinaryPath(command);
|
|
346
|
+
// Exit plumbing — resolved once, either on success or error
|
|
347
|
+
let resolveExit;
|
|
348
|
+
let exitResolved = false;
|
|
349
|
+
const exitPromise = new Promise((resolve) => {
|
|
350
|
+
resolveExit = (code) => {
|
|
351
|
+
if (exitResolved)
|
|
352
|
+
return;
|
|
353
|
+
exitResolved = true;
|
|
354
|
+
resolve(code);
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
// Set up stdin pipe for writeStdin/closeStdin — skip if FD 0 is already
|
|
358
|
+
// a PTY slave, pipe, or file (shell redirect/pipe wiring must be preserved)
|
|
359
|
+
const stdinIsPty = kernel.isatty(ctx.pid, 0);
|
|
360
|
+
const stdinAlreadyRouted = stdinIsPty || this._isFdKernelRouted(ctx.pid, 0) || this._isFdRegularFile(ctx.pid, 0);
|
|
361
|
+
let stdinWriteFd;
|
|
362
|
+
if (!stdinAlreadyRouted) {
|
|
363
|
+
const stdinPipe = kernel.pipe(ctx.pid);
|
|
364
|
+
kernel.fdDup2(ctx.pid, stdinPipe.readFd, 0);
|
|
365
|
+
kernel.fdClose(ctx.pid, stdinPipe.readFd);
|
|
366
|
+
stdinWriteFd = stdinPipe.writeFd;
|
|
367
|
+
}
|
|
368
|
+
const proc = {
|
|
369
|
+
onStdout: null,
|
|
370
|
+
onStderr: null,
|
|
371
|
+
onExit: null,
|
|
372
|
+
writeStdin: (data) => {
|
|
373
|
+
if (stdinWriteFd !== undefined)
|
|
374
|
+
kernel.fdWrite(ctx.pid, stdinWriteFd, data);
|
|
375
|
+
},
|
|
376
|
+
closeStdin: () => {
|
|
377
|
+
if (stdinWriteFd !== undefined) {
|
|
378
|
+
try {
|
|
379
|
+
kernel.fdClose(ctx.pid, stdinWriteFd);
|
|
380
|
+
}
|
|
381
|
+
catch { /* already closed */ }
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
kill: (signal) => {
|
|
385
|
+
const worker = this._activeWorkers.get(ctx.pid);
|
|
386
|
+
if (worker) {
|
|
387
|
+
worker.terminate();
|
|
388
|
+
this._activeWorkers.delete(ctx.pid);
|
|
389
|
+
}
|
|
390
|
+
// Encode signal-killed exit status (POSIX: low 7 bits = signal number)
|
|
391
|
+
const signalStatus = signal & 0x7f;
|
|
392
|
+
resolveExit(signalStatus);
|
|
393
|
+
proc.onExit?.(signalStatus);
|
|
394
|
+
},
|
|
395
|
+
wait: () => exitPromise,
|
|
396
|
+
};
|
|
397
|
+
// Launch worker asynchronously — spawn() returns synchronously per contract
|
|
398
|
+
this._launchWorker(command, args, ctx, proc, resolveExit, binaryPath).catch((err) => {
|
|
399
|
+
const errBytes = new TextEncoder().encode(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
400
|
+
ctx.onStderr?.(errBytes);
|
|
401
|
+
proc.onStderr?.(errBytes);
|
|
402
|
+
resolveExit(1);
|
|
403
|
+
proc.onExit?.(1);
|
|
404
|
+
});
|
|
405
|
+
return proc;
|
|
406
|
+
}
|
|
407
|
+
async dispose() {
|
|
408
|
+
for (const worker of this._activeWorkers.values()) {
|
|
409
|
+
try {
|
|
410
|
+
await worker.terminate();
|
|
411
|
+
}
|
|
412
|
+
catch { /* best effort */ }
|
|
413
|
+
}
|
|
414
|
+
this._activeWorkers.clear();
|
|
415
|
+
// Clean up TLS-upgraded sockets (kernel sockets cleaned up by kernel.dispose)
|
|
416
|
+
for (const sock of this._tlsSockets.values()) {
|
|
417
|
+
try {
|
|
418
|
+
sock.destroy();
|
|
419
|
+
}
|
|
420
|
+
catch { /* best effort */ }
|
|
421
|
+
}
|
|
422
|
+
this._tlsSockets.clear();
|
|
423
|
+
this._moduleCache.clear();
|
|
424
|
+
this._kernel = null;
|
|
425
|
+
}
|
|
426
|
+
// -------------------------------------------------------------------------
|
|
427
|
+
// Command discovery
|
|
428
|
+
// -------------------------------------------------------------------------
|
|
429
|
+
/** Scan all command directories, validating WASM magic bytes. */
|
|
430
|
+
async _scanCommandDirs() {
|
|
431
|
+
this._commandPaths.clear();
|
|
432
|
+
this._commands = [];
|
|
433
|
+
for (const dir of this._commandDirs) {
|
|
434
|
+
let entries;
|
|
435
|
+
try {
|
|
436
|
+
entries = await readdir(dir);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// Directory doesn't exist or isn't readable — skip
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
for (const entry of entries) {
|
|
443
|
+
// Skip dotfiles
|
|
444
|
+
if (entry.startsWith('.'))
|
|
445
|
+
continue;
|
|
446
|
+
const fullPath = join(dir, entry);
|
|
447
|
+
// Skip directories
|
|
448
|
+
try {
|
|
449
|
+
const st = await stat(fullPath);
|
|
450
|
+
if (st.isDirectory())
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
// Validate WASM magic bytes
|
|
457
|
+
if (!(await isWasmBinary(fullPath)))
|
|
458
|
+
continue;
|
|
459
|
+
// First directory containing the command wins (PATH semantics)
|
|
460
|
+
if (!this._commandPaths.has(entry)) {
|
|
461
|
+
this._commandPaths.set(entry, fullPath);
|
|
462
|
+
this._commands.push(entry);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/** Resolve permission tier for a command with wildcard and default tier support. */
|
|
468
|
+
_resolvePermissionTier(command) {
|
|
469
|
+
// No permissions config → fully unrestricted (backward compatible)
|
|
470
|
+
if (Object.keys(this._permissions).length === 0)
|
|
471
|
+
return 'full';
|
|
472
|
+
// User config checked first (exact, glob, *), defaults as fallback layer
|
|
473
|
+
return resolvePermissionTier(command, this._permissions, DEFAULT_FIRST_PARTY_TIERS);
|
|
474
|
+
}
|
|
475
|
+
/** Resolve binary path for a command. */
|
|
476
|
+
_resolveBinaryPath(command) {
|
|
477
|
+
// commandDirs mode: look up per-command binary path
|
|
478
|
+
const perCommand = this._commandPaths.get(command);
|
|
479
|
+
if (perCommand)
|
|
480
|
+
return perCommand;
|
|
481
|
+
// Legacy mode: all commands use a single binary
|
|
482
|
+
if (this._legacyMode)
|
|
483
|
+
return this._wasmBinaryPath;
|
|
484
|
+
// Fallback to wasmBinaryPath if set (shouldn't reach here normally)
|
|
485
|
+
return this._wasmBinaryPath;
|
|
486
|
+
}
|
|
487
|
+
// -------------------------------------------------------------------------
|
|
488
|
+
// FD helpers
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
/** Check if a process's FD is routed through kernel (pipe or PTY). */
|
|
491
|
+
_isFdKernelRouted(pid, fd) {
|
|
492
|
+
if (!this._kernel)
|
|
493
|
+
return false;
|
|
494
|
+
try {
|
|
495
|
+
const stat = this._kernel.fdStat(pid, fd);
|
|
496
|
+
if (stat.filetype === 6)
|
|
497
|
+
return true; // FILETYPE_PIPE
|
|
498
|
+
return this._kernel.isatty(pid, fd); // PTY slave
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** Check if a process's FD points to a regular file (e.g. shell < redirect). */
|
|
505
|
+
_isFdRegularFile(pid, fd) {
|
|
506
|
+
if (!this._kernel)
|
|
507
|
+
return false;
|
|
508
|
+
try {
|
|
509
|
+
const stat = this._kernel.fdStat(pid, fd);
|
|
510
|
+
return stat.filetype === 4; // FILETYPE_REGULAR_FILE
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// -------------------------------------------------------------------------
|
|
517
|
+
// Worker lifecycle
|
|
518
|
+
// -------------------------------------------------------------------------
|
|
519
|
+
async _launchWorker(command, args, ctx, proc, resolveExit, binaryPath) {
|
|
520
|
+
const kernel = this._kernel;
|
|
521
|
+
// Pre-compile module via cache for fast re-instantiation on subsequent spawns
|
|
522
|
+
let wasmModule;
|
|
523
|
+
try {
|
|
524
|
+
wasmModule = await this._moduleCache.resolve(binaryPath);
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
// Fail fast with a clear error — don't launch a worker with an undefined module
|
|
528
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
529
|
+
throw new Error(`wasmvm: failed to compile module for '${command}' at ${binaryPath}: ${msg}`);
|
|
530
|
+
}
|
|
531
|
+
// Create shared buffers for RPC
|
|
532
|
+
const signalBuf = new SharedArrayBuffer(SIGNAL_BUFFER_BYTES);
|
|
533
|
+
const dataBuf = new SharedArrayBuffer(DATA_BUFFER_BYTES);
|
|
534
|
+
// Check if stdio FDs are kernel-routed (pipe, PTY, or regular file redirect)
|
|
535
|
+
const stdinPiped = this._isFdKernelRouted(ctx.pid, 0);
|
|
536
|
+
const stdinIsFile = this._isFdRegularFile(ctx.pid, 0);
|
|
537
|
+
const stdoutPiped = this._isFdKernelRouted(ctx.pid, 1);
|
|
538
|
+
const stdoutIsFile = this._isFdRegularFile(ctx.pid, 1);
|
|
539
|
+
const stderrPiped = this._isFdKernelRouted(ctx.pid, 2);
|
|
540
|
+
const stderrIsFile = this._isFdRegularFile(ctx.pid, 2);
|
|
541
|
+
// Detect which FDs are TTYs (PTY slaves) for brush-shell interactive mode
|
|
542
|
+
const ttyFds = [];
|
|
543
|
+
for (const fd of [0, 1, 2]) {
|
|
544
|
+
if (kernel.isatty(ctx.pid, fd))
|
|
545
|
+
ttyFds.push(fd);
|
|
546
|
+
}
|
|
547
|
+
const permissionTier = this._resolvePermissionTier(command);
|
|
548
|
+
const workerData = {
|
|
549
|
+
wasmBinaryPath: binaryPath,
|
|
550
|
+
command,
|
|
551
|
+
args,
|
|
552
|
+
pid: ctx.pid,
|
|
553
|
+
ppid: ctx.ppid,
|
|
554
|
+
env: ctx.env,
|
|
555
|
+
cwd: ctx.cwd,
|
|
556
|
+
signalBuf,
|
|
557
|
+
dataBuf,
|
|
558
|
+
// Tell worker which stdio channels are kernel-routed (pipe, PTY, or file redirect)
|
|
559
|
+
stdinFd: (stdinPiped || stdinIsFile) ? 99 : undefined,
|
|
560
|
+
stdoutFd: (stdoutPiped || stdoutIsFile) ? 99 : undefined,
|
|
561
|
+
stderrFd: (stderrPiped || stderrIsFile) ? 99 : undefined,
|
|
562
|
+
ttyFds: ttyFds.length > 0 ? ttyFds : undefined,
|
|
563
|
+
wasmModule,
|
|
564
|
+
permissionTier,
|
|
565
|
+
};
|
|
566
|
+
const workerUrl = getKernelWorkerUrl();
|
|
567
|
+
this._workerAdapter.spawn(workerUrl, { workerData }).then((worker) => {
|
|
568
|
+
this._activeWorkers.set(ctx.pid, worker);
|
|
569
|
+
worker.onMessage((raw) => {
|
|
570
|
+
const msg = raw;
|
|
571
|
+
this._handleWorkerMessage(msg, ctx, kernel, signalBuf, dataBuf, proc, resolveExit);
|
|
572
|
+
});
|
|
573
|
+
worker.onError((err) => {
|
|
574
|
+
const errBytes = new TextEncoder().encode(`wasmvm: ${err.message}\n`);
|
|
575
|
+
ctx.onStderr?.(errBytes);
|
|
576
|
+
proc.onStderr?.(errBytes);
|
|
577
|
+
this._activeWorkers.delete(ctx.pid);
|
|
578
|
+
resolveExit(1);
|
|
579
|
+
proc.onExit?.(1);
|
|
580
|
+
});
|
|
581
|
+
worker.onExit((_code) => {
|
|
582
|
+
this._activeWorkers.delete(ctx.pid);
|
|
583
|
+
});
|
|
584
|
+
}, (err) => {
|
|
585
|
+
// Worker creation failed (binary not found, etc.)
|
|
586
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
587
|
+
const errBytes = new TextEncoder().encode(`wasmvm: ${errMsg}\n`);
|
|
588
|
+
ctx.onStderr?.(errBytes);
|
|
589
|
+
proc.onStderr?.(errBytes);
|
|
590
|
+
resolveExit(127);
|
|
591
|
+
proc.onExit?.(127);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
// -------------------------------------------------------------------------
|
|
595
|
+
// Worker message handling
|
|
596
|
+
// -------------------------------------------------------------------------
|
|
597
|
+
_handleWorkerMessage(msg, ctx, kernel, signalBuf, dataBuf, proc, resolveExit) {
|
|
598
|
+
switch (msg.type) {
|
|
599
|
+
case 'stdout':
|
|
600
|
+
ctx.onStdout?.(msg.data);
|
|
601
|
+
proc.onStdout?.(msg.data);
|
|
602
|
+
break;
|
|
603
|
+
case 'stderr':
|
|
604
|
+
ctx.onStderr?.(msg.data);
|
|
605
|
+
proc.onStderr?.(msg.data);
|
|
606
|
+
break;
|
|
607
|
+
case 'exit':
|
|
608
|
+
this._activeWorkers.delete(ctx.pid);
|
|
609
|
+
this._wasmPendingSignals.delete(ctx.pid);
|
|
610
|
+
resolveExit(msg.code);
|
|
611
|
+
proc.onExit?.(msg.code);
|
|
612
|
+
break;
|
|
613
|
+
case 'syscall':
|
|
614
|
+
this._handleSyscall(msg, ctx.pid, kernel, signalBuf, dataBuf);
|
|
615
|
+
break;
|
|
616
|
+
case 'ready':
|
|
617
|
+
// Worker is ready — could be used for stdin/lifecycle signaling
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// -------------------------------------------------------------------------
|
|
622
|
+
// Syscall RPC handler — dispatches worker requests to KernelInterface
|
|
623
|
+
// -------------------------------------------------------------------------
|
|
624
|
+
async _handleSyscall(msg, pid, kernel, signalBuf, dataBuf) {
|
|
625
|
+
const signal = new Int32Array(signalBuf);
|
|
626
|
+
const data = new Uint8Array(dataBuf);
|
|
627
|
+
let errno = 0;
|
|
628
|
+
let intResult = 0;
|
|
629
|
+
let responseData = null;
|
|
630
|
+
try {
|
|
631
|
+
switch (msg.call) {
|
|
632
|
+
case 'fdRead': {
|
|
633
|
+
const result = await kernel.fdRead(pid, msg.args.fd, msg.args.length);
|
|
634
|
+
if (result.length > DATA_BUFFER_BYTES) {
|
|
635
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
data.set(result, 0);
|
|
639
|
+
responseData = result;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case 'fdWrite': {
|
|
643
|
+
intResult = await kernel.fdWrite(pid, msg.args.fd, new Uint8Array(msg.args.data));
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case 'fdPread': {
|
|
647
|
+
const result = await kernel.fdPread(pid, msg.args.fd, msg.args.length, BigInt(msg.args.offset));
|
|
648
|
+
if (result.length > DATA_BUFFER_BYTES) {
|
|
649
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
data.set(result, 0);
|
|
653
|
+
responseData = result;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case 'fdPwrite': {
|
|
657
|
+
intResult = await kernel.fdPwrite(pid, msg.args.fd, new Uint8Array(msg.args.data), BigInt(msg.args.offset));
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case 'fdOpen': {
|
|
661
|
+
intResult = kernel.fdOpen(pid, msg.args.path, msg.args.flags, msg.args.mode);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case 'fdSeek': {
|
|
665
|
+
const offset = await kernel.fdSeek(pid, msg.args.fd, BigInt(msg.args.offset), msg.args.whence);
|
|
666
|
+
intResult = Number(offset);
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case 'fdClose': {
|
|
670
|
+
kernel.fdClose(pid, msg.args.fd);
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case 'fdStat': {
|
|
674
|
+
const stat = kernel.fdStat(pid, msg.args.fd);
|
|
675
|
+
// Pack stat into data buffer: filetype(i32) + flags(i32) + rights(f64 for bigint)
|
|
676
|
+
const view = new DataView(dataBuf);
|
|
677
|
+
view.setInt32(0, stat.filetype, true);
|
|
678
|
+
view.setInt32(4, stat.flags, true);
|
|
679
|
+
view.setFloat64(8, Number(stat.rights), true);
|
|
680
|
+
responseData = new Uint8Array(0); // signal data-in-buffer
|
|
681
|
+
Atomics.store(signal, SIG_IDX_DATA_LEN, 16);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case 'spawn': {
|
|
685
|
+
// proc_spawn → kernel.spawn() — the critical cross-runtime routing
|
|
686
|
+
// Includes FD overrides for pipe wiring (brush-shell pipeline stages)
|
|
687
|
+
const spawnCtx = {
|
|
688
|
+
env: msg.args.env,
|
|
689
|
+
cwd: msg.args.cwd,
|
|
690
|
+
ppid: pid,
|
|
691
|
+
};
|
|
692
|
+
// Forward FD overrides — only pass non-default values
|
|
693
|
+
const stdinFd = msg.args.stdinFd;
|
|
694
|
+
const stdoutFd = msg.args.stdoutFd;
|
|
695
|
+
const stderrFd = msg.args.stderrFd;
|
|
696
|
+
if (stdinFd !== undefined && stdinFd !== 0)
|
|
697
|
+
spawnCtx.stdinFd = stdinFd;
|
|
698
|
+
if (stdoutFd !== undefined && stdoutFd !== 1)
|
|
699
|
+
spawnCtx.stdoutFd = stdoutFd;
|
|
700
|
+
if (stderrFd !== undefined && stderrFd !== 2)
|
|
701
|
+
spawnCtx.stderrFd = stderrFd;
|
|
702
|
+
const managed = kernel.spawn(msg.args.command, msg.args.spawnArgs, spawnCtx);
|
|
703
|
+
intResult = managed.pid;
|
|
704
|
+
// Exit code is delivered via the waitpid RPC — no async write needed
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
case 'waitpid': {
|
|
708
|
+
const result = await kernel.waitpid(msg.args.pid, msg.args.options);
|
|
709
|
+
// WNOHANG returns null if process is still running (encode as -1 for WASM side)
|
|
710
|
+
intResult = result ? result.status : -1;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case 'kill': {
|
|
714
|
+
kernel.kill(msg.args.pid, msg.args.signal);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case 'sigaction': {
|
|
718
|
+
// proc_sigaction → register signal disposition in kernel process table
|
|
719
|
+
const sigNum = msg.args.signal;
|
|
720
|
+
const action = msg.args.action;
|
|
721
|
+
let handler;
|
|
722
|
+
if (action === 0) {
|
|
723
|
+
handler = 'default';
|
|
724
|
+
}
|
|
725
|
+
else if (action === 1) {
|
|
726
|
+
handler = 'ignore';
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
// action=2: user handler — queue signal for cooperative delivery
|
|
730
|
+
handler = (sig) => {
|
|
731
|
+
let queue = this._wasmPendingSignals.get(pid);
|
|
732
|
+
if (!queue) {
|
|
733
|
+
queue = [];
|
|
734
|
+
this._wasmPendingSignals.set(pid, queue);
|
|
735
|
+
}
|
|
736
|
+
queue.push(sig);
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
kernel.processTable.sigaction(pid, sigNum, { handler, mask: new Set(), flags: 0 });
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case 'pipe': {
|
|
743
|
+
// fd_pipe → create kernel pipe in this process's FD table
|
|
744
|
+
const pipeFds = kernel.pipe(pid);
|
|
745
|
+
// Pack read + write FDs: low 16 bits = readFd, high 16 bits = writeFd
|
|
746
|
+
intResult = (pipeFds.readFd & 0xFFFF) | ((pipeFds.writeFd & 0xFFFF) << 16);
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case 'openpty': {
|
|
750
|
+
// pty_open → allocate PTY master/slave pair in this process's FD table
|
|
751
|
+
const ptyFds = kernel.openpty(pid);
|
|
752
|
+
// Pack master + slave FDs: low 16 bits = masterFd, high 16 bits = slaveFd
|
|
753
|
+
intResult = (ptyFds.masterFd & 0xFFFF) | ((ptyFds.slaveFd & 0xFFFF) << 16);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case 'fdDup': {
|
|
757
|
+
intResult = kernel.fdDup(pid, msg.args.fd);
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
case 'fdDup2': {
|
|
761
|
+
kernel.fdDup2(pid, msg.args.oldFd, msg.args.newFd);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case 'fdDupMin': {
|
|
765
|
+
intResult = kernel.fdDupMin(pid, msg.args.fd, msg.args.minFd);
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
case 'vfsStat':
|
|
769
|
+
case 'vfsLstat': {
|
|
770
|
+
const path = scopedProcPath(pid, msg.args.path);
|
|
771
|
+
const stat = msg.call === 'vfsLstat'
|
|
772
|
+
? await kernel.vfs.lstat(path)
|
|
773
|
+
: await kernel.vfs.stat(path);
|
|
774
|
+
const enc = new TextEncoder();
|
|
775
|
+
const json = JSON.stringify({
|
|
776
|
+
ino: stat.ino,
|
|
777
|
+
type: stat.isDirectory ? 'dir' : stat.isSymbolicLink ? 'symlink' : 'file',
|
|
778
|
+
mode: stat.mode,
|
|
779
|
+
uid: stat.uid,
|
|
780
|
+
gid: stat.gid,
|
|
781
|
+
nlink: stat.nlink,
|
|
782
|
+
size: stat.size,
|
|
783
|
+
atime: stat.atimeMs,
|
|
784
|
+
mtime: stat.mtimeMs,
|
|
785
|
+
ctime: stat.ctimeMs,
|
|
786
|
+
});
|
|
787
|
+
const bytes = enc.encode(json);
|
|
788
|
+
if (bytes.length > DATA_BUFFER_BYTES) {
|
|
789
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
data.set(bytes, 0);
|
|
793
|
+
responseData = bytes;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
case 'vfsReaddir': {
|
|
797
|
+
const entries = await kernel.vfs.readDir(scopedProcPath(pid, msg.args.path));
|
|
798
|
+
const bytes = new TextEncoder().encode(JSON.stringify(entries));
|
|
799
|
+
if (bytes.length > DATA_BUFFER_BYTES) {
|
|
800
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
data.set(bytes, 0);
|
|
804
|
+
responseData = bytes;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
case 'vfsMkdir': {
|
|
808
|
+
await kernel.vfs.mkdir(scopedProcPath(pid, msg.args.path));
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case 'vfsUnlink': {
|
|
812
|
+
await kernel.vfs.removeFile(scopedProcPath(pid, msg.args.path));
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
case 'vfsRmdir': {
|
|
816
|
+
await kernel.vfs.removeDir(scopedProcPath(pid, msg.args.path));
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
case 'vfsRename': {
|
|
820
|
+
await kernel.vfs.rename(scopedProcPath(pid, msg.args.oldPath), scopedProcPath(pid, msg.args.newPath));
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case 'vfsSymlink': {
|
|
824
|
+
await kernel.vfs.symlink(msg.args.target, scopedProcPath(pid, msg.args.linkPath));
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case 'vfsReadlink': {
|
|
828
|
+
const normalizedPath = msg.args.path;
|
|
829
|
+
const target = normalizedPath === '/proc/self'
|
|
830
|
+
? '/proc/' + pid
|
|
831
|
+
: await kernel.vfs.readlink(scopedProcPath(pid, normalizedPath));
|
|
832
|
+
const bytes = new TextEncoder().encode(target);
|
|
833
|
+
if (bytes.length > DATA_BUFFER_BYTES) {
|
|
834
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
data.set(bytes, 0);
|
|
838
|
+
responseData = bytes;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
case 'vfsReadFile': {
|
|
842
|
+
const content = await kernel.vfs.readFile(scopedProcPath(pid, msg.args.path));
|
|
843
|
+
if (content.length > DATA_BUFFER_BYTES) {
|
|
844
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
data.set(content, 0);
|
|
848
|
+
responseData = content;
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
case 'vfsWriteFile': {
|
|
852
|
+
await kernel.vfs.writeFile(scopedProcPath(pid, msg.args.path), new Uint8Array(msg.args.data));
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
case 'vfsExists': {
|
|
856
|
+
const exists = await kernel.vfs.exists(scopedProcPath(pid, msg.args.path));
|
|
857
|
+
intResult = exists ? 1 : 0;
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case 'vfsRealpath': {
|
|
861
|
+
const normalizedPath = msg.args.path;
|
|
862
|
+
const resolved = normalizedPath === '/proc/self'
|
|
863
|
+
? '/proc/' + pid
|
|
864
|
+
: await kernel.vfs.realpath(scopedProcPath(pid, normalizedPath));
|
|
865
|
+
const bytes = new TextEncoder().encode(resolved);
|
|
866
|
+
if (bytes.length > DATA_BUFFER_BYTES) {
|
|
867
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
data.set(bytes, 0);
|
|
871
|
+
responseData = bytes;
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
// ----- Networking (TCP sockets via kernel socket table) -----
|
|
875
|
+
case 'netSocket': {
|
|
876
|
+
intResult = kernel.socketTable.create(normalizeSocketDomain(msg.args.domain), normalizeSocketType(msg.args.type), msg.args.protocol, pid);
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
case 'netConnect': {
|
|
880
|
+
const socketId = msg.args.fd;
|
|
881
|
+
const socket = kernel.socketTable.get(socketId);
|
|
882
|
+
const addr = msg.args.addr;
|
|
883
|
+
// Parse "host:port" or unix path
|
|
884
|
+
const lastColon = addr.lastIndexOf(':');
|
|
885
|
+
if (lastColon === -1) {
|
|
886
|
+
if (socket && socket.domain !== AF_UNIX) {
|
|
887
|
+
errno = ERRNO_MAP.EINVAL;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
// Unix domain socket path
|
|
891
|
+
await kernel.socketTable.connect(socketId, { path: addr });
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
const host = addr.slice(0, lastColon);
|
|
895
|
+
const port = parseInt(addr.slice(lastColon + 1), 10);
|
|
896
|
+
if (isNaN(port)) {
|
|
897
|
+
errno = ERRNO_MAP.EINVAL;
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
// Route through kernel socket table (host adapter handles real TCP)
|
|
901
|
+
await kernel.socketTable.connect(socketId, { host, port });
|
|
902
|
+
}
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
case 'netSend': {
|
|
906
|
+
const socketId = msg.args.fd;
|
|
907
|
+
// TLS-upgraded sockets write directly to host TLS socket
|
|
908
|
+
const tlsSock = this._tlsSockets.get(socketId);
|
|
909
|
+
if (tlsSock) {
|
|
910
|
+
const tlsData = Buffer.from(msg.args.data);
|
|
911
|
+
await new Promise((resolve, reject) => {
|
|
912
|
+
tlsSock.write(tlsData, (err) => err ? reject(err) : resolve());
|
|
913
|
+
});
|
|
914
|
+
intResult = tlsData.length;
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
const sendData = new Uint8Array(msg.args.data);
|
|
918
|
+
intResult = kernel.socketTable.send(socketId, sendData, msg.args.flags ?? 0);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
case 'netRecv': {
|
|
922
|
+
const socketId = msg.args.fd;
|
|
923
|
+
const maxLen = msg.args.length;
|
|
924
|
+
const flags = msg.args.flags ?? 0;
|
|
925
|
+
// TLS-upgraded sockets read directly from host TLS socket
|
|
926
|
+
const tlsRecvSock = this._tlsSockets.get(socketId);
|
|
927
|
+
if (tlsRecvSock) {
|
|
928
|
+
const tlsRecvData = await new Promise((resolve) => {
|
|
929
|
+
const onData = (chunk) => {
|
|
930
|
+
cleanupTls();
|
|
931
|
+
if (chunk.length > maxLen) {
|
|
932
|
+
tlsRecvSock.unshift(chunk.subarray(maxLen));
|
|
933
|
+
resolve(new Uint8Array(chunk.subarray(0, maxLen)));
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
resolve(new Uint8Array(chunk));
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
const onEnd = () => { cleanupTls(); resolve(new Uint8Array(0)); };
|
|
940
|
+
const onError = () => { cleanupTls(); resolve(new Uint8Array(0)); };
|
|
941
|
+
const cleanupTls = () => {
|
|
942
|
+
tlsRecvSock.removeListener('data', onData);
|
|
943
|
+
tlsRecvSock.removeListener('end', onEnd);
|
|
944
|
+
tlsRecvSock.removeListener('error', onError);
|
|
945
|
+
};
|
|
946
|
+
tlsRecvSock.once('data', onData);
|
|
947
|
+
tlsRecvSock.once('end', onEnd);
|
|
948
|
+
tlsRecvSock.once('error', onError);
|
|
949
|
+
});
|
|
950
|
+
if (tlsRecvData.length > DATA_BUFFER_BYTES) {
|
|
951
|
+
errno = 76;
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
if (tlsRecvData.length > 0)
|
|
955
|
+
data.set(tlsRecvData, 0);
|
|
956
|
+
responseData = tlsRecvData;
|
|
957
|
+
intResult = tlsRecvData.length;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
// Kernel socket recv — may need to wait for data from read pump
|
|
961
|
+
let recvResult = kernel.socketTable.recv(socketId, maxLen, flags);
|
|
962
|
+
if (recvResult === null) {
|
|
963
|
+
// Check if more data might arrive (socket still connected, EOF not received)
|
|
964
|
+
const ksock = kernel.socketTable.get(socketId);
|
|
965
|
+
if (ksock && (ksock.state === 'connected' || ksock.state === 'write-closed')) {
|
|
966
|
+
const mightHaveMore = ksock.external
|
|
967
|
+
? !ksock.peerWriteClosed
|
|
968
|
+
: (ksock.peerId !== undefined && !ksock.peerWriteClosed);
|
|
969
|
+
if (mightHaveMore) {
|
|
970
|
+
await ksock.readWaiters.enqueue(30000).wait();
|
|
971
|
+
recvResult = kernel.socketTable.recv(socketId, maxLen, flags);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const recvData = recvResult ?? new Uint8Array(0);
|
|
976
|
+
if (recvData.length > DATA_BUFFER_BYTES) {
|
|
977
|
+
errno = 76;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
if (recvData.length > 0)
|
|
981
|
+
data.set(recvData, 0);
|
|
982
|
+
responseData = recvData;
|
|
983
|
+
intResult = recvData.length;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
case 'netTlsConnect': {
|
|
987
|
+
const socketId = msg.args.fd;
|
|
988
|
+
// Access the kernel socket's host socket for TLS upgrade
|
|
989
|
+
const ksockTls = kernel.socketTable.get(socketId);
|
|
990
|
+
if (!ksockTls) {
|
|
991
|
+
errno = ERRNO_MAP.EBADF;
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
if (!ksockTls.external || !ksockTls.hostSocket) {
|
|
995
|
+
errno = ERRNO_MAP.EINVAL; // Can't TLS-upgrade loopback sockets
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
// Extract underlying net.Socket from host adapter
|
|
999
|
+
const realSock = ksockTls.hostSocket.socket;
|
|
1000
|
+
if (!realSock) {
|
|
1001
|
+
errno = ERRNO_MAP.EINVAL;
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
// Detach kernel read pump by clearing hostSocket
|
|
1005
|
+
ksockTls.hostSocket = undefined;
|
|
1006
|
+
const hostname = msg.args.hostname;
|
|
1007
|
+
const tlsOpts = {
|
|
1008
|
+
socket: realSock,
|
|
1009
|
+
servername: hostname, // SNI
|
|
1010
|
+
};
|
|
1011
|
+
if (msg.args.verifyPeer === false) {
|
|
1012
|
+
tlsOpts.rejectUnauthorized = false;
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
const tlsSock = await new Promise((resolve, reject) => {
|
|
1016
|
+
const s = tlsConnect(tlsOpts, () => resolve(s));
|
|
1017
|
+
s.on('error', reject);
|
|
1018
|
+
});
|
|
1019
|
+
// TLS socket bypasses kernel — send/recv go directly through _tlsSockets
|
|
1020
|
+
this._tlsSockets.set(socketId, tlsSock);
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
errno = ERRNO_MAP.ECONNREFUSED;
|
|
1024
|
+
}
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
case 'netGetaddrinfo': {
|
|
1028
|
+
const host = msg.args.host;
|
|
1029
|
+
const port = msg.args.port;
|
|
1030
|
+
try {
|
|
1031
|
+
// Resolve all addresses (IPv4 + IPv6)
|
|
1032
|
+
const result = await lookup(host, { all: true });
|
|
1033
|
+
const addresses = result.map((r) => ({
|
|
1034
|
+
addr: r.address,
|
|
1035
|
+
family: r.family,
|
|
1036
|
+
}));
|
|
1037
|
+
const json = JSON.stringify(addresses);
|
|
1038
|
+
const bytes = new TextEncoder().encode(json);
|
|
1039
|
+
if (bytes.length > DATA_BUFFER_BYTES) {
|
|
1040
|
+
errno = 76; // EIO — response exceeds SAB capacity
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
data.set(bytes, 0);
|
|
1044
|
+
responseData = bytes;
|
|
1045
|
+
intResult = bytes.length;
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
// dns.lookup returns ENOTFOUND for unknown hosts
|
|
1049
|
+
const code = err.code;
|
|
1050
|
+
if (code === 'ENOTFOUND' || code === 'EAI_NONAME' || code === 'ENODATA') {
|
|
1051
|
+
errno = ERRNO_MAP.ENOENT;
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
errno = ERRNO_MAP.EINVAL;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
case 'netSetsockopt': {
|
|
1060
|
+
const socketId = msg.args.fd;
|
|
1061
|
+
const optvalBytes = new Uint8Array(msg.args.optval);
|
|
1062
|
+
const optval = decodeSocketOptionValue(optvalBytes);
|
|
1063
|
+
kernel.socketTable.setsockopt(socketId, msg.args.level, msg.args.optname, optval);
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
case 'netGetsockopt': {
|
|
1067
|
+
const socketId = msg.args.fd;
|
|
1068
|
+
const optlen = msg.args.optvalLen;
|
|
1069
|
+
const optval = kernel.socketTable.getsockopt(socketId, msg.args.level, msg.args.optname);
|
|
1070
|
+
if (optval === undefined) {
|
|
1071
|
+
errno = ERRNO_MAP.EINVAL;
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
const encoded = encodeSocketOptionValue(optval, optlen);
|
|
1075
|
+
if (encoded.length > DATA_BUFFER_BYTES) {
|
|
1076
|
+
errno = ERRNO_EIO;
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
data.set(encoded, 0);
|
|
1080
|
+
responseData = encoded;
|
|
1081
|
+
intResult = encoded.length;
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
case 'kernelSocketGetLocalAddr': {
|
|
1085
|
+
const socketId = msg.args.fd;
|
|
1086
|
+
const addrBytes = new TextEncoder().encode(serializeSockAddr(kernel.socketTable.getLocalAddr(socketId)));
|
|
1087
|
+
if (addrBytes.length > DATA_BUFFER_BYTES) {
|
|
1088
|
+
errno = ERRNO_EIO;
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
data.set(addrBytes, 0);
|
|
1092
|
+
responseData = addrBytes;
|
|
1093
|
+
intResult = addrBytes.length;
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
case 'kernelSocketGetRemoteAddr': {
|
|
1097
|
+
const socketId = msg.args.fd;
|
|
1098
|
+
const addrBytes = new TextEncoder().encode(serializeSockAddr(kernel.socketTable.getRemoteAddr(socketId)));
|
|
1099
|
+
if (addrBytes.length > DATA_BUFFER_BYTES) {
|
|
1100
|
+
errno = ERRNO_EIO;
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
data.set(addrBytes, 0);
|
|
1104
|
+
responseData = addrBytes;
|
|
1105
|
+
intResult = addrBytes.length;
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case 'netPoll': {
|
|
1109
|
+
const fds = msg.args.fds;
|
|
1110
|
+
const timeout = msg.args.timeout;
|
|
1111
|
+
const pollKernel = kernel;
|
|
1112
|
+
const revents = [];
|
|
1113
|
+
let ready = 0;
|
|
1114
|
+
// WASI poll constants
|
|
1115
|
+
const POLLIN = 0x1;
|
|
1116
|
+
const POLLOUT = 0x2;
|
|
1117
|
+
const POLLHUP = 0x2000;
|
|
1118
|
+
const POLLNVAL = 0x4000;
|
|
1119
|
+
// Check readiness helper: kernel socket table first, then kernel FD table
|
|
1120
|
+
const checkFd = (fd, events) => {
|
|
1121
|
+
// TLS-upgraded sockets — use host socket readability
|
|
1122
|
+
const tlsSockPoll = this._tlsSockets.get(fd);
|
|
1123
|
+
if (tlsSockPoll) {
|
|
1124
|
+
let rev = 0;
|
|
1125
|
+
if ((events & POLLIN) && tlsSockPoll.readableLength > 0)
|
|
1126
|
+
rev |= POLLIN;
|
|
1127
|
+
if ((events & POLLOUT) && tlsSockPoll.writable)
|
|
1128
|
+
rev |= POLLOUT;
|
|
1129
|
+
if (tlsSockPoll.destroyed)
|
|
1130
|
+
rev |= POLLHUP;
|
|
1131
|
+
return rev;
|
|
1132
|
+
}
|
|
1133
|
+
// Kernel socket table
|
|
1134
|
+
const ksock = kernel.socketTable.get(fd);
|
|
1135
|
+
if (ksock) {
|
|
1136
|
+
const ps = kernel.socketTable.poll(fd);
|
|
1137
|
+
let rev = 0;
|
|
1138
|
+
if ((events & POLLIN) && ps.readable)
|
|
1139
|
+
rev |= POLLIN;
|
|
1140
|
+
if ((events & POLLOUT) && ps.writable)
|
|
1141
|
+
rev |= POLLOUT;
|
|
1142
|
+
if (ps.hangup)
|
|
1143
|
+
rev |= POLLHUP;
|
|
1144
|
+
return rev;
|
|
1145
|
+
}
|
|
1146
|
+
// Kernel FD table (pipes, files)
|
|
1147
|
+
try {
|
|
1148
|
+
const ps = kernel.fdPoll(pid, fd);
|
|
1149
|
+
if (ps.invalid)
|
|
1150
|
+
return POLLNVAL;
|
|
1151
|
+
let rev = 0;
|
|
1152
|
+
if ((events & POLLIN) && ps.readable)
|
|
1153
|
+
rev |= POLLIN;
|
|
1154
|
+
if ((events & POLLOUT) && ps.writable)
|
|
1155
|
+
rev |= POLLOUT;
|
|
1156
|
+
if (ps.hangup)
|
|
1157
|
+
rev |= POLLHUP;
|
|
1158
|
+
return rev;
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
return POLLNVAL;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
// Recompute readiness after each wait cycle.
|
|
1165
|
+
const refreshReadiness = () => {
|
|
1166
|
+
ready = 0;
|
|
1167
|
+
revents.length = 0;
|
|
1168
|
+
for (const entry of fds) {
|
|
1169
|
+
const rev = checkFd(entry.fd, entry.events);
|
|
1170
|
+
revents.push(rev);
|
|
1171
|
+
if (rev !== 0)
|
|
1172
|
+
ready++;
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
// Wait for any polled FD to change state, then re-check them all.
|
|
1176
|
+
const waitForFdActivity = async (waitMs) => {
|
|
1177
|
+
await new Promise((resolve) => {
|
|
1178
|
+
let settled = false;
|
|
1179
|
+
const cleanups = [];
|
|
1180
|
+
const finish = () => {
|
|
1181
|
+
if (settled)
|
|
1182
|
+
return;
|
|
1183
|
+
settled = true;
|
|
1184
|
+
for (const cleanup of cleanups)
|
|
1185
|
+
cleanup();
|
|
1186
|
+
resolve();
|
|
1187
|
+
};
|
|
1188
|
+
const timer = setTimeout(finish, waitMs);
|
|
1189
|
+
cleanups.push(() => clearTimeout(timer));
|
|
1190
|
+
for (const entry of fds) {
|
|
1191
|
+
const tlsSockWait = this._tlsSockets.get(entry.fd);
|
|
1192
|
+
if (tlsSockWait) {
|
|
1193
|
+
if (entry.events & POLLIN) {
|
|
1194
|
+
const onReadable = () => finish();
|
|
1195
|
+
const onEnd = () => finish();
|
|
1196
|
+
tlsSockWait.once('readable', onReadable);
|
|
1197
|
+
tlsSockWait.once('end', onEnd);
|
|
1198
|
+
cleanups.push(() => {
|
|
1199
|
+
tlsSockWait.removeListener('readable', onReadable);
|
|
1200
|
+
tlsSockWait.removeListener('end', onEnd);
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
const ksock = kernel.socketTable.get(entry.fd);
|
|
1206
|
+
if (ksock) {
|
|
1207
|
+
if (entry.events & POLLIN) {
|
|
1208
|
+
const waitQueue = ksock.state === 'listening'
|
|
1209
|
+
? ksock.acceptWaiters
|
|
1210
|
+
: ksock.readWaiters;
|
|
1211
|
+
const handle = waitQueue.enqueue();
|
|
1212
|
+
void handle.wait().then(finish);
|
|
1213
|
+
cleanups.push(() => waitQueue.remove(handle));
|
|
1214
|
+
}
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (!pollKernel.fdPollWait) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if ((entry.events & (POLLIN | POLLOUT)) === 0) {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
void pollKernel.fdPollWait(pid, entry.fd, waitMs).then(finish).catch(() => { });
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
};
|
|
1227
|
+
refreshReadiness();
|
|
1228
|
+
if (ready === 0 && timeout !== 0) {
|
|
1229
|
+
const deadline = timeout > 0 ? Date.now() + timeout : null;
|
|
1230
|
+
while (ready === 0) {
|
|
1231
|
+
const waitMs = timeout < 0
|
|
1232
|
+
? RPC_WAIT_TIMEOUT_MS
|
|
1233
|
+
: Math.max(0, deadline - Date.now());
|
|
1234
|
+
if (waitMs === 0) {
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
await waitForFdActivity(waitMs);
|
|
1238
|
+
refreshReadiness();
|
|
1239
|
+
if (timeout > 0 && Date.now() >= deadline) {
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
// Encode revents as JSON
|
|
1245
|
+
const pollJson = JSON.stringify(revents);
|
|
1246
|
+
const pollBytes = new TextEncoder().encode(pollJson);
|
|
1247
|
+
if (pollBytes.length > DATA_BUFFER_BYTES) {
|
|
1248
|
+
errno = ERRNO_EIO;
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
data.set(pollBytes, 0);
|
|
1252
|
+
responseData = pollBytes;
|
|
1253
|
+
intResult = ready;
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
case 'netBind': {
|
|
1257
|
+
const socketId = msg.args.fd;
|
|
1258
|
+
const socket = kernel.socketTable.get(socketId);
|
|
1259
|
+
const addr = msg.args.addr;
|
|
1260
|
+
// Parse "host:port" or unix path
|
|
1261
|
+
const lastColon = addr.lastIndexOf(':');
|
|
1262
|
+
if (lastColon === -1) {
|
|
1263
|
+
if (socket && socket.domain !== AF_UNIX) {
|
|
1264
|
+
errno = ERRNO_MAP.EINVAL;
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
// Unix domain socket path
|
|
1268
|
+
await kernel.socketTable.bind(socketId, { path: addr });
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
const host = addr.slice(0, lastColon);
|
|
1272
|
+
const port = parseInt(addr.slice(lastColon + 1), 10);
|
|
1273
|
+
if (isNaN(port)) {
|
|
1274
|
+
errno = ERRNO_MAP.EINVAL;
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
await kernel.socketTable.bind(socketId, { host, port });
|
|
1278
|
+
}
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
case 'netListen': {
|
|
1282
|
+
const socketId = msg.args.fd;
|
|
1283
|
+
const backlog = msg.args.backlog;
|
|
1284
|
+
await kernel.socketTable.listen(socketId, backlog);
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
case 'netAccept': {
|
|
1288
|
+
const socketId = msg.args.fd;
|
|
1289
|
+
// accept() returns null if no pending connection — wait for one
|
|
1290
|
+
let newSockId = kernel.socketTable.accept(socketId);
|
|
1291
|
+
if (newSockId === null) {
|
|
1292
|
+
const listenerSock = kernel.socketTable.get(socketId);
|
|
1293
|
+
if (listenerSock) {
|
|
1294
|
+
await listenerSock.acceptWaiters.enqueue(30000).wait();
|
|
1295
|
+
newSockId = kernel.socketTable.accept(socketId);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (newSockId === null) {
|
|
1299
|
+
errno = ERRNO_MAP.EAGAIN;
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
intResult = newSockId;
|
|
1303
|
+
// Return the remote address of the accepted socket
|
|
1304
|
+
const acceptedSock = kernel.socketTable.get(newSockId);
|
|
1305
|
+
let addrStr = '';
|
|
1306
|
+
if (acceptedSock?.remoteAddr) {
|
|
1307
|
+
addrStr = serializeSockAddr(acceptedSock.remoteAddr);
|
|
1308
|
+
}
|
|
1309
|
+
const addrBytes = new TextEncoder().encode(addrStr);
|
|
1310
|
+
if (addrBytes.length <= DATA_BUFFER_BYTES) {
|
|
1311
|
+
data.set(addrBytes, 0);
|
|
1312
|
+
responseData = addrBytes;
|
|
1313
|
+
}
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
case 'netSendTo': {
|
|
1317
|
+
const socketId = msg.args.fd;
|
|
1318
|
+
const sendData = new Uint8Array(msg.args.data);
|
|
1319
|
+
const flags = msg.args.flags ?? 0;
|
|
1320
|
+
const addr = msg.args.addr;
|
|
1321
|
+
// Parse "host:port" destination address
|
|
1322
|
+
const lastColon = addr.lastIndexOf(':');
|
|
1323
|
+
if (lastColon === -1) {
|
|
1324
|
+
errno = ERRNO_MAP.EINVAL;
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
const host = addr.slice(0, lastColon);
|
|
1328
|
+
const port = parseInt(addr.slice(lastColon + 1), 10);
|
|
1329
|
+
if (isNaN(port)) {
|
|
1330
|
+
errno = ERRNO_MAP.EINVAL;
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
intResult = kernel.socketTable.sendTo(socketId, sendData, flags, { host, port });
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
case 'netRecvFrom': {
|
|
1337
|
+
const socketId = msg.args.fd;
|
|
1338
|
+
const maxLen = msg.args.length;
|
|
1339
|
+
const flags = msg.args.flags ?? 0;
|
|
1340
|
+
// recvFrom may return null if no datagram queued — wait for one
|
|
1341
|
+
let result = kernel.socketTable.recvFrom(socketId, maxLen, flags);
|
|
1342
|
+
if (result === null) {
|
|
1343
|
+
const sock = kernel.socketTable.get(socketId);
|
|
1344
|
+
if (sock) {
|
|
1345
|
+
await sock.readWaiters.enqueue(30000).wait();
|
|
1346
|
+
result = kernel.socketTable.recvFrom(socketId, maxLen, flags);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (result === null) {
|
|
1350
|
+
errno = ERRNO_MAP.EAGAIN;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
// Pack [data | addr] into combined buffer, intResult = data length
|
|
1354
|
+
const addrStr = serializeSockAddr(result.srcAddr);
|
|
1355
|
+
const addrBytes = new TextEncoder().encode(addrStr);
|
|
1356
|
+
const combined = new Uint8Array(result.data.length + addrBytes.length);
|
|
1357
|
+
combined.set(result.data, 0);
|
|
1358
|
+
combined.set(addrBytes, result.data.length);
|
|
1359
|
+
if (combined.length > DATA_BUFFER_BYTES) {
|
|
1360
|
+
errno = ERRNO_EIO;
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
data.set(combined, 0);
|
|
1364
|
+
responseData = combined;
|
|
1365
|
+
intResult = result.data.length;
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
case 'netClose': {
|
|
1369
|
+
const socketId = msg.args.fd;
|
|
1370
|
+
// Clean up TLS socket if upgraded
|
|
1371
|
+
const tlsCleanup = this._tlsSockets.get(socketId);
|
|
1372
|
+
if (tlsCleanup) {
|
|
1373
|
+
tlsCleanup.destroy();
|
|
1374
|
+
this._tlsSockets.delete(socketId);
|
|
1375
|
+
}
|
|
1376
|
+
kernel.socketTable.close(socketId, pid);
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1379
|
+
default:
|
|
1380
|
+
errno = ERRNO_MAP.ENOSYS; // ENOSYS
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
catch (err) {
|
|
1384
|
+
errno = mapErrorToErrno(err);
|
|
1385
|
+
}
|
|
1386
|
+
// Guard against SAB data buffer overflow
|
|
1387
|
+
if (errno === 0 && responseData && responseData.length > DATA_BUFFER_BYTES) {
|
|
1388
|
+
errno = 76; // EIO — response exceeds 1MB SAB capacity
|
|
1389
|
+
responseData = null;
|
|
1390
|
+
}
|
|
1391
|
+
// Piggyback pending signal for cooperative delivery to WASM trampoline
|
|
1392
|
+
const pendingQueue = this._wasmPendingSignals.get(pid);
|
|
1393
|
+
const pendingSig = pendingQueue?.length ? pendingQueue.shift() : 0;
|
|
1394
|
+
// Write response to signal buffer — always set DATA_LEN so workers
|
|
1395
|
+
// never read stale lengths from previous calls (e.g. 0-byte EOF reads)
|
|
1396
|
+
Atomics.store(signal, SIG_IDX_DATA_LEN, responseData ? responseData.length : 0);
|
|
1397
|
+
Atomics.store(signal, SIG_IDX_ERRNO, errno);
|
|
1398
|
+
Atomics.store(signal, SIG_IDX_INT_RESULT, intResult);
|
|
1399
|
+
Atomics.store(signal, SIG_IDX_PENDING_SIGNAL, pendingSig);
|
|
1400
|
+
Atomics.store(signal, SIG_IDX_STATE, SIG_STATE_READY);
|
|
1401
|
+
Atomics.notify(signal, SIG_IDX_STATE);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
/** Map errors to WASI errno codes. Prefers structured .code, falls back to string matching. */
|
|
1405
|
+
export function mapErrorToErrno(err) {
|
|
1406
|
+
if (!(err instanceof Error))
|
|
1407
|
+
return ERRNO_EIO;
|
|
1408
|
+
// Prefer structured code field (KernelError, VfsError)
|
|
1409
|
+
const code = err.code;
|
|
1410
|
+
if (code && code in ERRNO_MAP)
|
|
1411
|
+
return ERRNO_MAP[code];
|
|
1412
|
+
// Fallback: match error code in message string
|
|
1413
|
+
const msg = err.message;
|
|
1414
|
+
for (const [name, errno] of Object.entries(ERRNO_MAP)) {
|
|
1415
|
+
if (msg.includes(name))
|
|
1416
|
+
return errno;
|
|
1417
|
+
}
|
|
1418
|
+
return ERRNO_EIO;
|
|
1419
|
+
}
|
|
1420
|
+
//# sourceMappingURL=driver.js.map
|