@volter/browser-wali 0.1.5 → 0.1.7
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/README.md +73 -0
- package/dist/browser-wali-command.d.ts +3 -0
- package/dist/browser-wali-command.d.ts.map +1 -1
- package/dist/browser-wali-command.js +2 -1
- package/dist/browser-wali-command.js.map +1 -1
- package/dist/capabilities.d.ts +73 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +86 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/child-processes.d.ts +24 -0
- package/dist/child-processes.d.ts.map +1 -0
- package/dist/child-processes.js +134 -0
- package/dist/child-processes.js.map +1 -0
- package/dist/codec-protocol.d.ts +66 -0
- package/dist/codec-protocol.d.ts.map +1 -0
- package/dist/codec-protocol.js +59 -0
- package/dist/codec-protocol.js.map +1 -0
- package/dist/codec-remote.d.ts +33 -0
- package/dist/codec-remote.d.ts.map +1 -0
- package/dist/codec-remote.js +190 -0
- package/dist/codec-remote.js.map +1 -0
- package/dist/codec-worker.d.ts +2 -0
- package/dist/codec-worker.d.ts.map +1 -0
- package/dist/codec-worker.js +438 -0
- package/dist/codec-worker.js.map +1 -0
- package/dist/coherent-file-locks.d.ts.map +1 -1
- package/dist/coherent-file-locks.js +17 -5
- package/dist/coherent-file-locks.js.map +1 -1
- package/dist/descriptors.d.ts +49 -0
- package/dist/descriptors.d.ts.map +1 -0
- package/dist/descriptors.js +115 -0
- package/dist/descriptors.js.map +1 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -2
- package/dist/events.js.map +1 -1
- package/dist/filesystem-protocol.d.ts +28 -1
- package/dist/filesystem-protocol.d.ts.map +1 -1
- package/dist/filesystem-protocol.js +18 -0
- package/dist/filesystem-protocol.js.map +1 -1
- package/dist/filesystem-remote.d.ts +26 -2
- package/dist/filesystem-remote.d.ts.map +1 -1
- package/dist/filesystem-remote.js +132 -23
- package/dist/filesystem-remote.js.map +1 -1
- package/dist/filesystem-worker.js +274 -34
- package/dist/filesystem-worker.js.map +1 -1
- package/dist/filesystem.d.ts +30 -1
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/filesystem.js +180 -50
- package/dist/filesystem.js.map +1 -1
- package/dist/fork-worker.d.ts +2 -0
- package/dist/fork-worker.d.ts.map +1 -0
- package/dist/fork-worker.js +52 -0
- package/dist/fork-worker.js.map +1 -0
- package/dist/host.d.ts +36 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +153 -0
- package/dist/host.js.map +1 -0
- package/dist/loopback-host.d.ts +25 -1
- package/dist/loopback-host.d.ts.map +1 -1
- package/dist/loopback-host.js +447 -13
- package/dist/loopback-host.js.map +1 -1
- package/dist/memory.d.ts +65 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +306 -49
- package/dist/memory.js.map +1 -1
- package/dist/module-layout.d.ts +2 -0
- package/dist/module-layout.d.ts.map +1 -1
- package/dist/module-layout.js +4 -2
- package/dist/module-layout.js.map +1 -1
- package/dist/monotonic-clock.d.ts +12 -0
- package/dist/monotonic-clock.d.ts.map +1 -0
- package/dist/monotonic-clock.js +14 -0
- package/dist/monotonic-clock.js.map +1 -0
- package/dist/network-protocol.d.ts +12 -1
- package/dist/network-protocol.d.ts.map +1 -1
- package/dist/network-protocol.js +6 -0
- package/dist/network-protocol.js.map +1 -1
- package/dist/network-worker.js +149 -20
- package/dist/network-worker.js.map +1 -1
- package/dist/network.d.ts +15 -3
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +74 -39
- package/dist/network.js.map +1 -1
- package/dist/process-adapter.d.ts +4 -1
- package/dist/process-adapter.d.ts.map +1 -1
- package/dist/process-adapter.js +3 -3
- package/dist/process-adapter.js.map +1 -1
- package/dist/process-bridge.d.ts +43 -0
- package/dist/process-bridge.d.ts.map +1 -1
- package/dist/process-bridge.js +30 -1
- package/dist/process-bridge.js.map +1 -1
- package/dist/program.d.ts +60 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +92 -31
- package/dist/program.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +491 -134
- package/dist/runtime.js.map +1 -1
- package/dist/select.d.ts +16 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +100 -0
- package/dist/select.js.map +1 -0
- package/dist/shared-pipes.d.ts +17 -6
- package/dist/shared-pipes.d.ts.map +1 -1
- package/dist/shared-pipes.js +94 -70
- package/dist/shared-pipes.js.map +1 -1
- package/dist/signals.d.ts +48 -0
- package/dist/signals.d.ts.map +1 -0
- package/dist/signals.js +232 -0
- package/dist/signals.js.map +1 -0
- package/dist/snapshot-filesystem.d.ts +128 -2
- package/dist/snapshot-filesystem.d.ts.map +1 -1
- package/dist/snapshot-filesystem.js +481 -53
- package/dist/snapshot-filesystem.js.map +1 -1
- package/dist/spawn-policy.d.ts +5 -0
- package/dist/spawn-policy.d.ts.map +1 -0
- package/dist/spawn-policy.js +29 -0
- package/dist/spawn-policy.js.map +1 -0
- package/dist/stdin.d.ts +2 -0
- package/dist/stdin.d.ts.map +1 -1
- package/dist/stdin.js +4 -0
- package/dist/stdin.js.map +1 -1
- package/dist/symlink.d.ts +8 -0
- package/dist/symlink.d.ts.map +1 -1
- package/dist/symlink.js +13 -0
- package/dist/symlink.js.map +1 -1
- package/dist/system.d.ts +152 -1
- package/dist/system.d.ts.map +1 -1
- package/dist/system.js +226 -7
- package/dist/system.js.map +1 -1
- package/dist/thread-coordinator-worker.d.ts +1 -0
- package/dist/thread-coordinator-worker.d.ts.map +1 -1
- package/dist/thread-coordinator-worker.js +1 -0
- package/dist/thread-coordinator-worker.js.map +1 -1
- package/dist/thread-worker.js +241 -122
- package/dist/thread-worker.js.map +1 -1
- package/dist/threads.d.ts +12 -2
- package/dist/threads.d.ts.map +1 -1
- package/dist/threads.js +30 -10
- package/dist/threads.js.map +1 -1
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/websocket-peer.d.ts.map +1 -1
- package/dist/websocket-peer.js +4 -14
- package/dist/websocket-peer.js.map +1 -1
- package/dist/worker-program-protocol.d.ts +70 -3
- package/dist/worker-program-protocol.d.ts.map +1 -1
- package/dist/worker-program-worker.js +115 -23
- package/dist/worker-program-worker.js.map +1 -1
- package/dist/worker-program.d.ts +25 -1
- package/dist/worker-program.d.ts.map +1 -1
- package/dist/worker-program.js +470 -61
- package/dist/worker-program.js.map +1 -1
- package/dist/worker-standby.d.ts +3 -0
- package/dist/worker-standby.d.ts.map +1 -0
- package/dist/worker-standby.js +17 -0
- package/dist/worker-standby.js.map +1 -0
- package/package.json +20 -6
package/dist/thread-worker.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { WaliSignals, WaliSignalHost } from "./signals.js";
|
|
2
|
+
import { createConfinedWaliSpawnImport } from "./spawn-policy.js";
|
|
3
|
+
import { normalizeProcessFilesystemPolicy, intersectProcessFilesystemPolicies } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
|
|
1
4
|
import { WaliMemoryManager } from "./memory.js";
|
|
2
5
|
import { WaliNetworkHost } from "./network.js";
|
|
3
6
|
import { SharedWaliStdinReader } from "./stdin.js";
|
|
4
|
-
import { WaliSystemHost } from "./system.js";
|
|
7
|
+
import { futexRequeue, WaliSystemHost } from "./system.js";
|
|
5
8
|
import { WaliFileDescriptorTable } from "./filesystem.js";
|
|
6
9
|
import { WaliFilesystemClient, WaliSharedFileDescriptorHost } from "./filesystem-remote.js";
|
|
10
|
+
import { createWaliCodecImports, WaliCodecClient } from "./codec-remote.js";
|
|
11
|
+
import { createSelectImport, SELECT_SCRATCH_BYTES } from "./select.js";
|
|
12
|
+
import { WALI_ENOSYS, waliUnimplementedTraps } from "./capabilities.js";
|
|
7
13
|
import { WaliSharedPipeHost } from "./shared-pipes.js";
|
|
8
|
-
import { BROWSER_PROCESS_RESPONSE_BYTES,
|
|
14
|
+
import { BROWSER_PROCESS_RESPONSE_BYTES, closeSharedPipe, decodeProcessSnapshot, retainSharedPipe, } from "./process-bridge.js";
|
|
15
|
+
import { monotonicMilliseconds } from "./monotonic-clock.js";
|
|
9
16
|
class ThreadExit extends Error {
|
|
10
17
|
}
|
|
11
18
|
const scope = globalThis;
|
|
@@ -21,14 +28,22 @@ let mmapCursor;
|
|
|
21
28
|
let sharedMmapRegistry;
|
|
22
29
|
let fatalState;
|
|
23
30
|
let networkChannel;
|
|
31
|
+
let networkHost;
|
|
32
|
+
let signalState;
|
|
33
|
+
let signalHost;
|
|
24
34
|
let filesystemChannel;
|
|
35
|
+
let codecChannel;
|
|
25
36
|
let stdin;
|
|
37
|
+
let terminalAttached = false;
|
|
26
38
|
let stdioAliasRegistry;
|
|
27
39
|
let sharedPipes;
|
|
28
40
|
let childPidAllocator;
|
|
29
41
|
let browserProcessPort;
|
|
42
|
+
let childFilesystemPolicyVersion;
|
|
43
|
+
let inheritedFilesystemPolicy;
|
|
30
44
|
let outputPort;
|
|
31
45
|
let slotIndex = -1;
|
|
46
|
+
let poolSize = 1;
|
|
32
47
|
let coordinatorPort;
|
|
33
48
|
let diagnosticLength;
|
|
34
49
|
let diagnosticBytes;
|
|
@@ -39,6 +54,27 @@ const reportedUnsupportedImports = new Set();
|
|
|
39
54
|
const sleepWord = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
40
55
|
const diagnosticHistory = [];
|
|
41
56
|
const DIAGNOSTIC_HISTORY_LIMIT = 32;
|
|
57
|
+
/**
|
|
58
|
+
* THE DIAGNOSTIC RING. A pthread records every syscall it makes (the call and
|
|
59
|
+
* its result) so that a fatal can print the thread's last moments. It used to
|
|
60
|
+
* rebuild the whole history on EVERY write -- join 32 lines, encode them,
|
|
61
|
+
* fill(0) the 16 KiB shared buffer, copy -- twice per syscall, on threads
|
|
62
|
+
* whose work IS syscalls (oneTBB workers park and wake on futex). Measured
|
|
63
|
+
* 2026-09-20: the courtyard's depsgraph updates ran 1.3-1.7x and its mesh
|
|
64
|
+
* copies 2-3x behind the Emscripten skew while the booleans themselves were
|
|
65
|
+
* at parity. Now each write encodes ONE line into a fixed slot of the ring
|
|
66
|
+
* (`encodeInto`, no allocation) and bumps a counter; `threads.ts` reads the
|
|
67
|
+
* last DIAGNOSTIC_HISTORY_LIMIT slots in order when it needs them.
|
|
68
|
+
* Layout, shared with `WaliThreadPool.diagnostics`: Int32 [0] = lines written
|
|
69
|
+
* (monotonic); slot i at DIAGNOSTIC_SLOT_OFFSET + i * DIAGNOSTIC_SLOT_BYTES,
|
|
70
|
+
* a little-endian u16 length then the UTF-8 bytes, truncated to fit.
|
|
71
|
+
*/
|
|
72
|
+
const DIAGNOSTIC_SLOT_BYTES = 480;
|
|
73
|
+
const DIAGNOSTIC_SLOT_OFFSET = Int32Array.BYTES_PER_ELEMENT;
|
|
74
|
+
const diagnosticEncoder = new TextEncoder();
|
|
75
|
+
// `encodeInto` refuses a view over shared memory; the line is encoded into
|
|
76
|
+
// this private scratch and copied into the slot.
|
|
77
|
+
const diagnosticScratch = new Uint8Array(DIAGNOSTIC_SLOT_BYTES - 2);
|
|
42
78
|
const DETACHED_THREAD_RUNTIME_NOTICE = new TextEncoder().encode("== Detached Thread End: unmapself TBD");
|
|
43
79
|
scope.onmessage = (event) => {
|
|
44
80
|
if (event.data.kind === "initialize") {
|
|
@@ -53,6 +89,8 @@ scope.onmessage = (event) => {
|
|
|
53
89
|
};
|
|
54
90
|
function runThread(message) {
|
|
55
91
|
threadId = message.tid;
|
|
92
|
+
if (signalHost)
|
|
93
|
+
signalHost.mask = message.signalMask ?? 0n;
|
|
56
94
|
diagnosticHistory.length = 0;
|
|
57
95
|
writeDiagnostic(`tid ${threadId} started function ${message.functionPointer}`);
|
|
58
96
|
try {
|
|
@@ -111,18 +149,24 @@ async function initialize(message) {
|
|
|
111
149
|
sharedPipes = message.sharedPipes;
|
|
112
150
|
childPidAllocator = new Int32Array(message.childPidAllocator);
|
|
113
151
|
browserProcessPort = message.browserProcessPort;
|
|
152
|
+
childFilesystemPolicyVersion = message.childFilesystemPolicyVersion;
|
|
153
|
+
inheritedFilesystemPolicy = message.inheritedFilesystemPolicy === undefined ? undefined : normalizeProcessFilesystemPolicy(message.inheritedFilesystemPolicy);
|
|
114
154
|
browserProcessPort?.start();
|
|
115
155
|
outputPort = message.outputPort;
|
|
116
156
|
outputPort?.start();
|
|
117
157
|
slotIndex = message.slotIndex;
|
|
158
|
+
poolSize = message.poolSize;
|
|
118
159
|
coordinatorPort = message.coordinatorPort;
|
|
119
160
|
coordinatorPort.onmessage = (event) => runThread(event.data);
|
|
120
161
|
coordinatorPort.start();
|
|
121
162
|
diagnosticLength = new Int32Array(message.diagnostic, 0, 1);
|
|
122
163
|
diagnosticBytes = new Uint8Array(message.diagnostic, Int32Array.BYTES_PER_ELEMENT);
|
|
123
164
|
stdin = message.stdin ? new SharedWaliStdinReader(message.stdin) : undefined;
|
|
165
|
+
terminalAttached = message.terminal === true;
|
|
124
166
|
networkChannel = message.network;
|
|
167
|
+
signalState = message.signals;
|
|
125
168
|
filesystemChannel = message.filesystem;
|
|
169
|
+
codecChannel = message.codec;
|
|
126
170
|
const imports = createImports();
|
|
127
171
|
instance = await WebAssembly.instantiate(message.module, imports);
|
|
128
172
|
scope.postMessage({ kind: "ready" });
|
|
@@ -134,26 +178,41 @@ async function initialize(message) {
|
|
|
134
178
|
function createImports() {
|
|
135
179
|
const wali = {};
|
|
136
180
|
const handlers = threadHandlers();
|
|
181
|
+
let selectScratch = 0;
|
|
182
|
+
handlers.SYS_select ??= createSelectImport(requiredMemory, (pointer, count, timeout) => handlers.SYS_poll(pointer, count, timeout), () => (selectScratch ||= Number(handlers.SYS_mmap(0, SELECT_SCRATCH_BYTES, 3, 0x22, -1, 0))));
|
|
137
183
|
if (networkChannel) {
|
|
138
184
|
const fallback = { ...handlers };
|
|
139
|
-
Object.assign(handlers,
|
|
185
|
+
Object.assign(handlers, networkHost.imports(fallback));
|
|
140
186
|
}
|
|
141
187
|
for (const descriptor of descriptors) {
|
|
142
188
|
if (descriptor.module !== "wali" || descriptor.kind !== "function")
|
|
143
189
|
continue;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
190
|
+
// The SAME question the root instance asks (`runtime.ts`'s
|
|
191
|
+
// `unimplementedImport`), from the same file: -ENOSYS where a caller can
|
|
192
|
+
// act on it, a trap for the four names where it cannot. This half already
|
|
193
|
+
// answered -38n for everything while the root killed the process for
|
|
194
|
+
// everything, so whether a program survived an unimplemented syscall
|
|
195
|
+
// depended on WHICH THREAD made the call.
|
|
196
|
+
const handler = handlers[descriptor.name] ?? (waliUnimplementedTraps(descriptor.name)
|
|
197
|
+
? () => { throw new Error(`Unsupported WALI imports: ${descriptor.name}`); }
|
|
198
|
+
: () => {
|
|
199
|
+
if (!reportedUnsupportedImports.has(descriptor.name)) {
|
|
200
|
+
reportedUnsupportedImports.add(descriptor.name);
|
|
201
|
+
const notice = `[WALI pthread ${threadId}] ${descriptor.name} is not implemented; answering -ENOSYS (38) as a kernel would.`;
|
|
202
|
+
console.error(notice);
|
|
203
|
+
// The same door the root instance uses, over the port this thread
|
|
204
|
+
// already holds: a notice from a pthread is no less the host's.
|
|
205
|
+
outputPort?.postMessage({ type: "diagnostic", message: notice, tid: threadId });
|
|
206
|
+
}
|
|
207
|
+
return WALI_ENOSYS;
|
|
208
|
+
});
|
|
151
209
|
wali[descriptor.name] = (...args) => {
|
|
152
210
|
const call = `${descriptor.name}(${args.map(String).join(",")})`;
|
|
153
211
|
writeDiagnostic(call);
|
|
154
212
|
try {
|
|
155
213
|
const result = handler(...args);
|
|
156
214
|
writeDiagnostic(`${call} => ${String(result)}`);
|
|
215
|
+
signalHost?.deliver();
|
|
157
216
|
return result;
|
|
158
217
|
}
|
|
159
218
|
catch (error) {
|
|
@@ -173,7 +232,7 @@ function createImports() {
|
|
|
173
232
|
};
|
|
174
233
|
}
|
|
175
234
|
function threadHandlers() {
|
|
176
|
-
const systemImports = new WaliSystemHost(requiredMemory, processPid, stdin, fatalState).imports();
|
|
235
|
+
const systemImports = new WaliSystemHost(requiredMemory, processPid, stdin, fatalState, terminalAttached, poolSize).imports();
|
|
177
236
|
const ioctl = systemImports.SYS_ioctl;
|
|
178
237
|
const filesystemClient = filesystemChannel ? new WaliFilesystemClient(filesystemChannel) : undefined;
|
|
179
238
|
const fileDescriptors = filesystemChannel ? new WaliFileDescriptorTable({
|
|
@@ -189,21 +248,100 @@ function threadHandlers() {
|
|
|
189
248
|
return BigInt(value.byteLength);
|
|
190
249
|
},
|
|
191
250
|
...(stdioAliasRegistry ? { stdioAliasRegistry } : {}),
|
|
251
|
+
...(sharedPipes ? { descriptorState: sharedPipes } : {}),
|
|
192
252
|
specialDescriptorMinimum: 10_000,
|
|
193
253
|
descriptorMaximum: 19_999,
|
|
194
254
|
}) : undefined;
|
|
195
255
|
const localFileImports = fileDescriptors?.imports() ?? {};
|
|
196
256
|
const sharedFileHost = filesystemClient
|
|
197
|
-
? new WaliSharedFileDescriptorHost(filesystemClient, stdioAliasRegistry) : undefined;
|
|
257
|
+
? new WaliSharedFileDescriptorHost(filesystemClient, stdioAliasRegistry, sharedPipes) : undefined;
|
|
198
258
|
const memoryManager = new WaliMemoryManager(requiredMemory, (fd, offset, length) => sharedFileHost?.readForMapping(fd, offset, length)
|
|
199
259
|
?? fileDescriptors?.readForMapping(fd, offset, length), requiredMmapCursor(), (fd) => sharedFileHost?.mappingPath(fd) ?? fileDescriptors?.mappingPath(fd), (path, offset, value) => sharedFileHost?.writeForMapping(path, offset, value)
|
|
200
|
-
?? fileDescriptors?.writeForMapping(path, offset, value) ?? false, requiredSharedMmapRegistry());
|
|
260
|
+
?? fileDescriptors?.writeForMapping(path, offset, value) ?? false, requiredSharedMmapRegistry(), (message) => { outputPort?.postMessage({ type: "diagnostic", message, tid: threadId }); });
|
|
261
|
+
signalHost = signalState ? new WaliSignalHost(new WaliSignals(signalState), requiredMemory, () => instance?.exports.__indirect_function_table, () => Number(memoryManager.imports().SYS_mmap(0, 4096, 3, 0x22, -1, 0))) : undefined;
|
|
262
|
+
networkHost = networkChannel ? new WaliNetworkHost(requiredMemory, networkChannel, signalHost) : undefined;
|
|
201
263
|
const fileImports = sharedFileHost?.imports(localFileImports) ?? localFileImports;
|
|
202
|
-
const sharedPipeHost = sharedPipes ? new WaliSharedPipeHost(requiredMemory, sharedPipes) : undefined;
|
|
264
|
+
const sharedPipeHost = sharedPipes ? new WaliSharedPipeHost(requiredMemory, sharedPipes, undefined, filesystemClient) : undefined;
|
|
203
265
|
const pipeImports = sharedPipeHost?.imports(fileImports) ?? fileImports;
|
|
204
|
-
const
|
|
266
|
+
const spawnBrowserProcess = (args, filesystemPolicy) => {
|
|
267
|
+
const [pathname, argvPointer, envPointer, cwdPointer, stdinFd, stdoutFd, stderrFd] = args;
|
|
268
|
+
if (!browserProcessPort || !childPidAllocator || !sharedPipeHost || !filesystemClient || !networkHost)
|
|
269
|
+
return -38;
|
|
270
|
+
if (inheritedFilesystemPolicy) {
|
|
271
|
+
if (childFilesystemPolicyVersion !== 1)
|
|
272
|
+
return -95;
|
|
273
|
+
filesystemPolicy = intersectProcessFilesystemPolicies(inheritedFilesystemPolicy, filesystemPolicy);
|
|
274
|
+
}
|
|
275
|
+
const executablePath = readCString(Number(pathname));
|
|
276
|
+
const argv = readCStringArray(Number(argvPointer));
|
|
277
|
+
if (argv.length === 0)
|
|
278
|
+
argv.push(executablePath);
|
|
279
|
+
const env = {};
|
|
280
|
+
for (const entry of readCStringArray(Number(envPointer))) {
|
|
281
|
+
const separator = entry.indexOf("=");
|
|
282
|
+
if (separator > 0)
|
|
283
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
284
|
+
}
|
|
285
|
+
const childPid = Atomics.add(childPidAllocator, 0, 1);
|
|
286
|
+
const process = {
|
|
287
|
+
control: new SharedArrayBuffer(8 * Int32Array.BYTES_PER_ELEMENT),
|
|
288
|
+
response: new SharedArrayBuffer(BROWSER_PROCESS_RESPONSE_BYTES),
|
|
289
|
+
snapshotApplied: false,
|
|
290
|
+
};
|
|
291
|
+
const exportPipe = (fd, role) => {
|
|
292
|
+
const handle = sharedPipeHost.exportBrowserProcessPipe(fd);
|
|
293
|
+
return handle?.role === role ? retainSharedPipe(handle) : undefined;
|
|
294
|
+
};
|
|
295
|
+
const stdin = exportPipe(Number(stdinFd), "reader");
|
|
296
|
+
const stdout = exportPipe(Number(stdoutFd), "writer");
|
|
297
|
+
const stderr = exportPipe(Number(stderrFd), "writer");
|
|
298
|
+
try {
|
|
299
|
+
const registered = networkHost.registerChild(childPid, process);
|
|
300
|
+
if (registered < 0)
|
|
301
|
+
throw new Error(`Child registration failed: ${registered}`);
|
|
302
|
+
const message = {
|
|
303
|
+
type: "spawn",
|
|
304
|
+
pid: childPid,
|
|
305
|
+
parentPid: processPid,
|
|
306
|
+
childPidAllocator: childPidAllocator.buffer,
|
|
307
|
+
descriptorTable: filesystemClient.copyForExecChannel([Number(stdinFd), Number(stdoutFd), Number(stderrFd)]),
|
|
308
|
+
...(filesystemPolicy ? { filesystemPolicy } : {}),
|
|
309
|
+
executablePath,
|
|
310
|
+
argv,
|
|
311
|
+
env,
|
|
312
|
+
...(Number(envPointer) === 0 ? { inheritEnvironment: true } : {}),
|
|
313
|
+
cwd: Number(cwdPointer) === 0 ? processCwd : readCString(Number(cwdPointer)),
|
|
314
|
+
control: process.control,
|
|
315
|
+
response: process.response,
|
|
316
|
+
// Mutations are already mirrored continuously by the process
|
|
317
|
+
// filesystem coordinator; helper launch must not serialize the VFS.
|
|
318
|
+
filesystemPatch: { files: [], removed: [] },
|
|
319
|
+
...(stdin ? { stdin } : {}),
|
|
320
|
+
...(stdout ? { stdout } : {}),
|
|
321
|
+
...(stderr ? { stderr } : {}),
|
|
322
|
+
};
|
|
323
|
+
filesystemClient.prepareSpawn();
|
|
324
|
+
browserProcessPort.postMessage(message, [message.descriptorTable.port, message.descriptorTable.releasePort]);
|
|
325
|
+
return childPid;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
networkHost.forgetChild(childPid);
|
|
329
|
+
if (stdin)
|
|
330
|
+
closeSharedPipe(stdin);
|
|
331
|
+
if (stdout)
|
|
332
|
+
closeSharedPipe(stdout);
|
|
333
|
+
if (stderr)
|
|
334
|
+
closeSharedPipe(stderr);
|
|
335
|
+
return -5;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
// A pthread holds its own channel to the process's one codec worker. No
|
|
339
|
+
// channel means the process did not negotiate the bridge: every codec
|
|
340
|
+
// import answers -95 before an argument is read.
|
|
341
|
+
const codecClient = codecChannel ? new WaliCodecClient(codecChannel) : undefined;
|
|
205
342
|
return {
|
|
206
343
|
...systemImports,
|
|
344
|
+
...createWaliCodecImports(requiredMemory, () => codecClient !== undefined, () => codecClient),
|
|
207
345
|
__init: () => 0,
|
|
208
346
|
__deinit: () => 0,
|
|
209
347
|
__proc_exit: () => { throw new ThreadExit(); },
|
|
@@ -219,7 +357,42 @@ function threadHandlers() {
|
|
|
219
357
|
},
|
|
220
358
|
__wasm_thread_spawn: (functionPointer, argumentPointer) => spawnNestedThread(Number(functionPointer), Number(argumentPointer)),
|
|
221
359
|
SYS_exit: () => { throw new ThreadExit(); },
|
|
360
|
+
/**
|
|
361
|
+
* A WORKER ENDING THE WHOLE PROCESS SAYS SO, BY THREAD.
|
|
362
|
+
*
|
|
363
|
+
* This path is how a pthread ends the process: the status goes in slot 2,
|
|
364
|
+
* slot 0 goes to 2, and `system.ts`'s `throwIfPthreadFailed` turns that
|
|
365
|
+
* into `WaliPthreadProcessExit`, which `runtime.ts` makes the process exit
|
|
366
|
+
* code. The host then sees a clean `exited <status>` with NOTHING naming
|
|
367
|
+
* the thread that did it -- and a status of 0 reads as an orderly
|
|
368
|
+
* shutdown, which is the most misleading answer this runtime can give.
|
|
369
|
+
*
|
|
370
|
+
* MEASURED 2026-09-19 (WS-W), and it cost a battery: headless Blender on
|
|
371
|
+
* this runtime ended mid-call twice -- `17-workshop-interior` at its
|
|
372
|
+
* seq 19, `19-tram-stop` at its seq 28, both immediately after a Cycles
|
|
373
|
+
* render -- reported to the editor as "Blender exited 0". Blender calls
|
|
374
|
+
* no `exit()` there. musl does: `wali-musl/src/thread/pthread_create.c`,
|
|
375
|
+
* `__pthread_exit`, "if this is the only thread in the list, don't
|
|
376
|
+
* proceed with termination of the thread ... exit(0)". A departing worker
|
|
377
|
+
* that finds `self->next == self` ends the PROCESS. So the question a
|
|
378
|
+
* reader needs answered first is WHICH THREAD CALLED THIS, and until this
|
|
379
|
+
* line existed nothing anywhere recorded it.
|
|
380
|
+
*
|
|
381
|
+
* The notice is the same door `runtime.ts` uses for the unimplemented
|
|
382
|
+
* syscall: `console.error` for devtools and `{type:"diagnostic"}` over
|
|
383
|
+
* the port this thread already holds, so it reaches the host's
|
|
384
|
+
* `onDiagnostic` and the editor's console rather than only a nested
|
|
385
|
+
* worker's devtools. It is not stdout: a kernel does not write to your
|
|
386
|
+
* streams.
|
|
387
|
+
*/
|
|
222
388
|
SYS_exit_group: (status) => {
|
|
389
|
+
const notice = `[WALI pthread ${threadId}] SYS_exit_group(${Number(status) & 0xff})`
|
|
390
|
+
+ ` -- a WORKER thread, not the main thread, is ending the process.`
|
|
391
|
+
+ ` If the program did not ask to exit, read musl's __pthread_exit:`
|
|
392
|
+
+ ` a departing thread that finds itself alone on the thread list calls exit(0).`;
|
|
393
|
+
writeDiagnostic(notice);
|
|
394
|
+
console.error(notice);
|
|
395
|
+
outputPort?.postMessage({ type: "diagnostic", message: notice, tid: threadId });
|
|
223
396
|
if (fatalState) {
|
|
224
397
|
Atomics.store(fatalState, 2, Number(status) & 0xff);
|
|
225
398
|
Atomics.store(fatalState, 0, 2);
|
|
@@ -247,7 +420,9 @@ function threadHandlers() {
|
|
|
247
420
|
SYS_sigaltstack: (_next, previous) => zeroMemory(Number(previous), 12),
|
|
248
421
|
SYS_mprotect: () => 0n,
|
|
249
422
|
SYS_munmap: () => 0n,
|
|
250
|
-
|
|
423
|
+
// All six of the kernel's arguments: FUTEX_REQUEUE reads the timeout slot
|
|
424
|
+
// as a count and needs `uaddr2` and `val3` as well. See futexRequeue.
|
|
425
|
+
SYS_futex: (address, operation, value, timeout, address2, value3) => futex(Number(address), Number(operation), Number(value), Number(timeout), Number(address2 ?? 0n), Number(value3 ?? 0n)),
|
|
251
426
|
SYS_clock_gettime: (clock, pointer) => clockGettime(Number(clock), Number(pointer)),
|
|
252
427
|
SYS_clock_nanosleep: (clock, flags, request, remaining) => clockNanosleep(Number(clock), Number(flags), Number(request), Number(remaining)),
|
|
253
428
|
SYS_nanosleep: (request, remaining) => clockNanosleep(1, 0, Number(request), Number(remaining)),
|
|
@@ -255,75 +430,27 @@ function threadHandlers() {
|
|
|
255
430
|
SYS_epoll_pwait: (_fd, _events, _maximum, timeout) => idlePoll(Number(timeout)),
|
|
256
431
|
SYS_gettimeofday: (pointer) => gettimeofday(Number(pointer)),
|
|
257
432
|
SYS_getrandom: (pointer, length) => getrandom(Number(pointer), Number(length)),
|
|
258
|
-
SYS_browser_spawn: (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const argv = readCStringArray(Number(argvPointer));
|
|
263
|
-
if (argv.length === 0)
|
|
264
|
-
argv.push(executablePath);
|
|
265
|
-
const env = {};
|
|
266
|
-
for (const entry of readCStringArray(Number(envPointer))) {
|
|
267
|
-
const separator = entry.indexOf("=");
|
|
268
|
-
if (separator > 0)
|
|
269
|
-
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
270
|
-
}
|
|
271
|
-
const childPid = Atomics.add(childPidAllocator, 0, 1);
|
|
272
|
-
const process = {
|
|
273
|
-
control: new SharedArrayBuffer(8 * Int32Array.BYTES_PER_ELEMENT),
|
|
274
|
-
response: new SharedArrayBuffer(BROWSER_PROCESS_RESPONSE_BYTES),
|
|
275
|
-
snapshotApplied: false,
|
|
276
|
-
};
|
|
277
|
-
const exportPipe = (fd, role) => {
|
|
278
|
-
const handle = sharedPipeHost.exportBrowserProcessPipe(fd);
|
|
279
|
-
return handle?.role === role ? retainSharedPipe(handle) : undefined;
|
|
280
|
-
};
|
|
281
|
-
const stdin = exportPipe(Number(stdinFd), "reader");
|
|
282
|
-
const stdout = exportPipe(Number(stdoutFd), "writer");
|
|
283
|
-
const stderr = exportPipe(Number(stderrFd), "writer");
|
|
284
|
-
childProcesses.set(childPid, process);
|
|
285
|
-
try {
|
|
286
|
-
const message = {
|
|
287
|
-
type: "spawn",
|
|
288
|
-
pid: childPid,
|
|
289
|
-
executablePath,
|
|
290
|
-
argv,
|
|
291
|
-
env,
|
|
292
|
-
cwd: Number(cwdPointer) === 0 ? processCwd : readCString(Number(cwdPointer)),
|
|
293
|
-
control: process.control,
|
|
294
|
-
response: process.response,
|
|
295
|
-
// Mutations are already mirrored continuously by the process
|
|
296
|
-
// filesystem coordinator; helper launch must not serialize the VFS.
|
|
297
|
-
filesystemPatch: { files: [], removed: [] },
|
|
298
|
-
...(stdin ? { stdin } : {}),
|
|
299
|
-
...(stdout ? { stdout } : {}),
|
|
300
|
-
...(stderr ? { stderr } : {}),
|
|
301
|
-
};
|
|
302
|
-
browserProcessPort.postMessage(message);
|
|
303
|
-
return childPid;
|
|
304
|
-
}
|
|
305
|
-
catch {
|
|
306
|
-
childProcesses.delete(childPid);
|
|
307
|
-
if (stdin)
|
|
308
|
-
closeSharedPipe(stdin);
|
|
309
|
-
if (stdout)
|
|
310
|
-
closeSharedPipe(stdout);
|
|
311
|
-
if (stderr)
|
|
312
|
-
closeSharedPipe(stderr);
|
|
313
|
-
return -5;
|
|
314
|
-
}
|
|
315
|
-
},
|
|
316
|
-
SYS_wait4: (requestedPid, status, options) => filesystemClient
|
|
317
|
-
? waitForChild(childProcesses, filesystemClient, Number(requestedPid), Number(status), Number(options))
|
|
433
|
+
SYS_browser_spawn: (...args) => spawnBrowserProcess(args),
|
|
434
|
+
SYS_browser_spawn_confined_v1: createConfinedWaliSpawnImport(() => memory, () => childFilesystemPolicyVersion === 1, spawnBrowserProcess),
|
|
435
|
+
SYS_wait4: (requestedPid, status, options) => filesystemClient && networkHost
|
|
436
|
+
? waitForChild(networkHost, filesystemClient, Number(requestedPid), Number(status), Number(options), signalHost?.mask ?? 0n)
|
|
318
437
|
: -38n,
|
|
319
438
|
SYS_kill: (requestedPid, signal) => {
|
|
439
|
+
// `kill(getpid(), 0)` is a liveness probe and a thread asks it of its own
|
|
440
|
+
// process as the main thread does (`runtime.ts`'s `signalSelf`). It was
|
|
441
|
+
// answered ESRCH, and a program that probes its own pid from a worker
|
|
442
|
+
// thread (Volter Harness's `process_is_live`, from a tokio worker) read itself
|
|
443
|
+
// as dead.
|
|
444
|
+
if (Number(signal) === 0 && (Number(requestedPid) === 0 || Number(requestedPid) === processPid))
|
|
445
|
+
return 0n;
|
|
320
446
|
const childPid = Math.abs(Number(requestedPid));
|
|
321
|
-
if (!
|
|
447
|
+
if (!networkHost?.hasChild(childPid) || !browserProcessPort)
|
|
322
448
|
return -3n;
|
|
323
449
|
browserProcessPort.postMessage({ type: "kill", pid: childPid, signal: Number(signal) });
|
|
324
450
|
return 0n;
|
|
325
451
|
},
|
|
326
452
|
SYS_setpgid: () => 0n,
|
|
453
|
+
SYS_pidfd_open: () => -38n,
|
|
327
454
|
SYS_open: () => -2n,
|
|
328
455
|
SYS_openat: () => -2n,
|
|
329
456
|
SYS_stat: () => -2n,
|
|
@@ -350,6 +477,7 @@ function threadHandlers() {
|
|
|
350
477
|
SYS_fcntl: (fd, command, argument) => fcntl(Number(fd), Number(command), Number(BigInt.asIntN(32, BigInt(argument)))),
|
|
351
478
|
...memoryManager.imports(),
|
|
352
479
|
...pipeImports,
|
|
480
|
+
...signalHost?.imports(),
|
|
353
481
|
SYS_ioctl: (fd, request, argument) => sharedPipeHost?.ioctl(Number(fd), Number(request))
|
|
354
482
|
?? fileDescriptors?.ioctl(Number(fd), Number(request))
|
|
355
483
|
?? ioctl(stdioDescriptor(Number(fd)) ?? Number(fd), request, argument),
|
|
@@ -362,20 +490,24 @@ function spawnNestedThread(functionPointer, argumentPointer) {
|
|
|
362
490
|
if (!coordinatorPort)
|
|
363
491
|
return -11;
|
|
364
492
|
const response = new Int32Array(new SharedArrayBuffer(2 * Int32Array.BYTES_PER_ELEMENT));
|
|
365
|
-
coordinatorPort.postMessage({ kind: "spawn", functionPointer, argumentPointer, response: response.buffer });
|
|
493
|
+
coordinatorPort.postMessage({ kind: "spawn", functionPointer, argumentPointer, signalMask: signalHost?.mask ?? 0n, response: response.buffer });
|
|
366
494
|
Atomics.wait(response, 0, 0);
|
|
367
495
|
return Atomics.load(response, 1);
|
|
368
496
|
}
|
|
369
497
|
function writeDiagnostic(message) {
|
|
370
498
|
if (!diagnosticLength || !diagnosticBytes)
|
|
371
499
|
return;
|
|
500
|
+
const written = Atomics.load(diagnosticLength, 0);
|
|
501
|
+
const slot = written % DIAGNOSTIC_HISTORY_LIMIT;
|
|
502
|
+
const at = DIAGNOSTIC_SLOT_OFFSET - Int32Array.BYTES_PER_ELEMENT + slot * DIAGNOSTIC_SLOT_BYTES;
|
|
372
503
|
diagnosticHistory.push(message);
|
|
373
504
|
if (diagnosticHistory.length > DIAGNOSTIC_HISTORY_LIMIT)
|
|
374
505
|
diagnosticHistory.shift();
|
|
375
|
-
const
|
|
376
|
-
diagnosticBytes.
|
|
377
|
-
diagnosticBytes
|
|
378
|
-
|
|
506
|
+
const { written: length } = diagnosticEncoder.encodeInto(message, diagnosticScratch);
|
|
507
|
+
diagnosticBytes.set(diagnosticScratch.subarray(0, length), at + 2);
|
|
508
|
+
diagnosticBytes[at] = length & 0xff;
|
|
509
|
+
diagnosticBytes[at + 1] = length >>> 8;
|
|
510
|
+
Atomics.store(diagnosticLength, 0, written + 1);
|
|
379
511
|
}
|
|
380
512
|
function readlink(pathPointer, outputPointer, length) {
|
|
381
513
|
if (readCString(pathPointer) !== "/proc/self/exe")
|
|
@@ -466,42 +598,18 @@ function readCStringArray(pointer) {
|
|
|
466
598
|
}
|
|
467
599
|
throw new WebAssembly.RuntimeError("WALI process string array is not terminated.");
|
|
468
600
|
}
|
|
469
|
-
function waitForChild(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return -10n;
|
|
482
|
-
childPid = requestedPid;
|
|
483
|
-
process = exact;
|
|
484
|
-
}
|
|
485
|
-
const control = new Int32Array(process.control);
|
|
486
|
-
if (Atomics.load(control, 0) === 0) {
|
|
487
|
-
if ((options & 1) !== 0)
|
|
488
|
-
return 0n;
|
|
489
|
-
while (Atomics.load(control, 0) === 0)
|
|
490
|
-
Atomics.wait(control, 0, 0, 1_000);
|
|
491
|
-
}
|
|
492
|
-
if (!process.snapshotApplied) {
|
|
493
|
-
process.snapshotApplied = true;
|
|
494
|
-
const patch = decodeProcessSnapshot(process);
|
|
495
|
-
if (patch)
|
|
496
|
-
applyProcessFilesystemPatch(filesystem, patch);
|
|
497
|
-
}
|
|
498
|
-
if (statusPointer !== 0) {
|
|
499
|
-
if (statusPointer < 0 || statusPointer + 4 > bytes().byteLength)
|
|
500
|
-
return -14n;
|
|
501
|
-
view().setInt32(statusPointer, (Atomics.load(control, 1) & 0xff) << 8, true);
|
|
502
|
-
}
|
|
503
|
-
children.delete(childPid);
|
|
504
|
-
return BigInt(childPid);
|
|
601
|
+
function waitForChild(host, filesystem, requestedPid, statusPointer, options, mask) {
|
|
602
|
+
if (statusPointer !== 0 && (statusPointer < 0 || statusPointer + 4 > bytes().byteLength))
|
|
603
|
+
return -14n;
|
|
604
|
+
const { result, process } = host.waitChild(requestedPid, options, mask);
|
|
605
|
+
if (result <= 0)
|
|
606
|
+
return BigInt(result);
|
|
607
|
+
const patch = decodeProcessSnapshot(process);
|
|
608
|
+
if (patch)
|
|
609
|
+
filesystem.importPeerPatch(patch);
|
|
610
|
+
if (statusPointer !== 0)
|
|
611
|
+
view().setInt32(statusPointer, (Atomics.load(new Int32Array(process.control), 1) & 0xff) << 8, true);
|
|
612
|
+
return BigInt(result);
|
|
505
613
|
}
|
|
506
614
|
function requiredMemory() {
|
|
507
615
|
if (!memory)
|
|
@@ -525,7 +633,7 @@ function zeroMemory(pointer, length) {
|
|
|
525
633
|
bytes().fill(0, pointer, pointer + length);
|
|
526
634
|
return 0n;
|
|
527
635
|
}
|
|
528
|
-
function futex(address, operation, value, timeoutPointer) {
|
|
636
|
+
function futex(address, operation, value, timeoutPointer, address2 = 0, value3 = 0) {
|
|
529
637
|
if (address % 4 !== 0)
|
|
530
638
|
return -22n;
|
|
531
639
|
const words = new Int32Array(requiredMemory().buffer);
|
|
@@ -533,6 +641,17 @@ function futex(address, operation, value, timeoutPointer) {
|
|
|
533
641
|
const command = operation & 0x7f;
|
|
534
642
|
if (command === 1 || command === 10)
|
|
535
643
|
return BigInt(Atomics.notify(words, index, Math.max(0, value)));
|
|
644
|
+
// A pthread worker is where a missing requeue actually strands a thread: its
|
|
645
|
+
// wait below is a single Atomics.wait with no timeout, so a waiter that the
|
|
646
|
+
// kernel would have requeued and woken is parked for the life of the
|
|
647
|
+
// process. futexRequeue carries the whole argument.
|
|
648
|
+
if (command === 3 || command === 4) {
|
|
649
|
+
if (address2 % 4 !== 0)
|
|
650
|
+
return -22n;
|
|
651
|
+
if (address2 < 0 || address2 + 4 > requiredMemory().buffer.byteLength)
|
|
652
|
+
return -22n;
|
|
653
|
+
return futexRequeue(words, index, command, value, timeoutPointer, value3);
|
|
654
|
+
}
|
|
536
655
|
if (command !== 0 && command !== 9)
|
|
537
656
|
return -38n;
|
|
538
657
|
let timeout;
|
|
@@ -543,14 +662,14 @@ function futex(address, operation, value, timeoutPointer) {
|
|
|
543
662
|
return -22n;
|
|
544
663
|
const milliseconds = Number(seconds * 1000n + nanoseconds / 1000000n);
|
|
545
664
|
timeout = command === 9
|
|
546
|
-
? Math.max(0, milliseconds - ((operation & 0x100) !== 0 ? Date.now() :
|
|
665
|
+
? Math.max(0, milliseconds - ((operation & 0x100) !== 0 ? Date.now() : monotonicMilliseconds()))
|
|
547
666
|
: milliseconds;
|
|
548
667
|
}
|
|
549
668
|
const result = Atomics.wait(words, index, value, timeout);
|
|
550
669
|
return result === "not-equal" ? -11n : result === "timed-out" ? -110n : 0n;
|
|
551
670
|
}
|
|
552
671
|
function clockGettime(clock, pointer) {
|
|
553
|
-
const milliseconds = clock === 0 ? performance.timeOrigin + performance.now() :
|
|
672
|
+
const milliseconds = clock === 0 ? performance.timeOrigin + performance.now() : monotonicMilliseconds();
|
|
554
673
|
const nanoseconds = BigInt(Math.floor(milliseconds * 1_000_000));
|
|
555
674
|
view().setBigInt64(pointer, nanoseconds / 1000000000n, true);
|
|
556
675
|
view().setBigInt64(pointer + 8, nanoseconds % 1000000000n, true);
|
|
@@ -573,7 +692,7 @@ function clockNanosleep(clock, flags, request, remaining) {
|
|
|
573
692
|
return -22n;
|
|
574
693
|
let duration = Number(seconds * 1000n + nanoseconds / 1000000n);
|
|
575
694
|
if (flags & 1)
|
|
576
|
-
duration = Math.max(0, duration - (clock === 0 ? Date.now() :
|
|
695
|
+
duration = Math.max(0, duration - (clock === 0 ? Date.now() : monotonicMilliseconds()));
|
|
577
696
|
Atomics.wait(sleepWord, 0, 0, duration);
|
|
578
697
|
if (remaining !== 0)
|
|
579
698
|
bytes().fill(0, remaining, remaining + 16);
|