@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
|
@@ -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
|