@threadedstack/wmd-web 0.0.0-stage → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +2601 -2
  2. package/RELEASE.json +123 -0
  3. package/export-page/coi-sw.js +20 -0
  4. package/export-page/index.html +17 -0
  5. package/export-page/run.css +10 -0
  6. package/export-page/run.js +236 -0
  7. package/guest/arm64/Image +4 -0
  8. package/guest/arm64/Image-4k +4 -0
  9. package/guest/arm64/initramfs.cpio.gz +0 -0
  10. package/guest/arm64/vm.wasm +0 -0
  11. package/package.json +36 -4
  12. package/page/sw.js +381 -0
  13. package/relay/control.ts +139 -0
  14. package/relay/engine.ts +787 -0
  15. package/relay/hosted.ts +660 -0
  16. package/relay/keys.ts +53 -0
  17. package/relay/local-tls.ts +121 -0
  18. package/relay/lock.ts +81 -0
  19. package/relay/logfile.ts +138 -0
  20. package/relay/main.ts +642 -0
  21. package/relay/policy.ts +241 -0
  22. package/relay/relay.ts +266 -0
  23. package/relay/token.ts +116 -0
  24. package/relay/v1.ts +107 -0
  25. package/relay/wisp.ts +615 -0
  26. package/src/base.js +325 -0
  27. package/src/boot.js +2037 -0
  28. package/src/calib-worker.js +40 -0
  29. package/src/fs/9p/codec.js +303 -0
  30. package/src/fs/9p/server.js +521 -0
  31. package/src/fs/errno.js +43 -0
  32. package/src/fs/graph.js +380 -0
  33. package/src/fs/overlay.js +1018 -0
  34. package/src/fs/pack-write.js +165 -0
  35. package/src/fs/pack.js +427 -0
  36. package/src/fs/seed.js +22 -0
  37. package/src/fs/svc-snapshot.js +65 -0
  38. package/src/fs/upper-pack.js +112 -0
  39. package/src/host/base-cache.js +195 -0
  40. package/src/host/base-file.js +328 -0
  41. package/src/host/base-io.js +93 -0
  42. package/src/host/boot-events.js +209 -0
  43. package/src/host/chan-service.js +300 -0
  44. package/src/host/chan-services.js +689 -0
  45. package/src/host/chan.js +655 -0
  46. package/src/host/ckpt-file.js +395 -0
  47. package/src/host/ckpt-io.js +207 -0
  48. package/src/host/ckpt-point.js +160 -0
  49. package/src/host/ckpt-save.js +64 -0
  50. package/src/host/ckpt.js +434 -0
  51. package/src/host/clock.js +111 -0
  52. package/src/host/ctl.js +778 -0
  53. package/src/host/errno.js +18 -0
  54. package/src/host/fs.js +167 -0
  55. package/src/host/gcnudge.js +83 -0
  56. package/src/host/i64shim.js +81 -0
  57. package/src/host/jit-cache.js +341 -0
  58. package/src/host/jit-hub-core.js +208 -0
  59. package/src/host/jit-hub.js +93 -0
  60. package/src/host/jit-snap.js +226 -0
  61. package/src/host/jit.js +1903 -0
  62. package/src/host/layout.js +100 -0
  63. package/src/host/monitor.js +110 -0
  64. package/src/host/p9mount.js +27 -0
  65. package/src/host/ram-mem.js +125 -0
  66. package/src/host/stdio.js +143 -0
  67. package/src/host/threads.js +109 -0
  68. package/src/host/wasi.js +335 -0
  69. package/src/host/wmd-imports.js +71 -0
  70. package/src/host/yamux.js +387 -0
  71. package/src/image-files.js +154 -0
  72. package/src/net/dhcp-dns.js +126 -0
  73. package/src/net/net-worker.js +172 -0
  74. package/src/net/packet.js +227 -0
  75. package/src/net/peer.js +230 -0
  76. package/src/net/proto.js +189 -0
  77. package/src/net/relay-client.js +226 -0
  78. package/src/net/stack.js +867 -0
  79. package/src/net/switch.js +160 -0
  80. package/src/net/wisp-client.js +570 -0
  81. package/src/net/wisp-transport.js +196 -0
  82. package/src/net/wisp.js +511 -0
  83. package/src/registry/client.js +176 -0
  84. package/src/registry/converter.js +68 -0
  85. package/src/registry/hub.js +221 -0
  86. package/src/registry/oci.js +76 -0
  87. package/src/registry/pack-worker.js +70 -0
  88. package/src/registry/pull.js +258 -0
  89. package/src/registry/ref.js +110 -0
  90. package/src/registry/wasi-mem.js +355 -0
  91. package/src/worker.js +315 -0
  92. package/wmdpack.wasm +0 -0
@@ -0,0 +1,335 @@
1
+ // The WASI preview1 subset vm.wasm imports (web/docs/host-interface.md §1.2, §2.5),
2
+ // modelled on the reference host runner/src/wasi.rs.
3
+ //
4
+ // One instance of these functions exists per Worker; everything that must be
5
+ // process-wide (fd table, clock base, exit flag) lives in shared buffers.
6
+
7
+ import { E } from './errno.js'
8
+ import { CTL_EXIT, CTL_EXIT_CODE } from './layout.js'
9
+
10
+ const FILETYPE_OFF = 16 // filestat.filetype
11
+ const CHUNK = 1 << 20
12
+
13
+ /** Thrown out of proc_exit to unwind the calling Worker's wasm stack. */
14
+ export class WasiExit extends Error {
15
+ constructor(code) {
16
+ super(`proc_exit(${code})`)
17
+ this.code = code
18
+ }
19
+ }
20
+
21
+ const enc = new TextEncoder()
22
+
23
+ /** NUL-terminated strings packed back to back, plus their offsets (args/environ layout). */
24
+ export function encodeStrings(list) {
25
+ const parts = list.map(s => enc.encode(s))
26
+ const size = parts.reduce((n, p) => n + p.length + 1, 0)
27
+ const buf = new Uint8Array(size)
28
+ const offsets = []
29
+ let o = 0
30
+ for (const p of parts) {
31
+ offsets.push(o)
32
+ buf.set(p, o)
33
+ o += p.length + 1 // trailing 0 already there
34
+ }
35
+ return { buf, offsets, size }
36
+ }
37
+
38
+ /**
39
+ * @param {object} o
40
+ * @param {WebAssembly.Memory} o.memory
41
+ * @param {string[]} o.args argv, argv[0] included
42
+ * @param {string[]} o.env "K=V" strings
43
+ * @param {ReturnType<import('./fs.js').createFs>} o.fs
44
+ * @param {ReturnType<import('./clock.js').createClock>} o.clock
45
+ * @param {Int32Array} o.ctl control cells
46
+ * @param {(fd:number, bytes:Uint8Array)=>void} o.write stdout/stderr sink
47
+ * @param {(code:number)=>void} [o.onExit] called once before WasiExit is thrown
48
+ * @param {(ms:number)=>void} [o.sleep] blocking sleep (Atomics.wait in Workers)
49
+ */
50
+ export function createWasi({ memory, args, env, fs, clock, ctl, write, onExit, sleep }) {
51
+ const argv = encodeStrings(args)
52
+ const envv = encodeStrings(env)
53
+ const sleepCell = new Int32Array(new SharedArrayBuffer(4))
54
+ const doSleep = sleep || (ms => Atomics.wait(sleepCell, 0, 0, ms))
55
+
56
+ let buf = null
57
+ let u8, dv
58
+ const views = () => {
59
+ const b = memory.buffer
60
+ if (b !== buf || b.byteLength !== u8.length) {
61
+ buf = b
62
+ u8 = new Uint8Array(b)
63
+ dv = new DataView(b)
64
+ }
65
+ }
66
+ // Initialise eagerly so views() can compare lengths.
67
+ buf = memory.buffer
68
+ u8 = new Uint8Array(buf)
69
+ dv = new DataView(buf)
70
+
71
+ const inRange = (ptr, len) => ptr >= 0 && len >= 0 && ptr + len <= u8.length
72
+ const u32 = x => x >>> 0
73
+
74
+ function putStrings(vec, ptrs, bufPtr) {
75
+ views()
76
+ ptrs = u32(ptrs)
77
+ bufPtr = u32(bufPtr)
78
+ if (!inRange(bufPtr, vec.size) || !inRange(ptrs, 4 * vec.offsets.length)) return E.FAULT
79
+ u8.set(vec.buf, bufPtr)
80
+ vec.offsets.forEach((o, i) => dv.setUint32(ptrs + 4 * i, bufPtr + o, true))
81
+ return E.SUCCESS
82
+ }
83
+ function putSizes(vec, countPtr, sizePtr) {
84
+ views()
85
+ countPtr = u32(countPtr)
86
+ sizePtr = u32(sizePtr)
87
+ if (!inRange(countPtr, 4) || !inRange(sizePtr, 4)) return E.FAULT
88
+ dv.setUint32(countPtr, vec.offsets.length, true)
89
+ dv.setUint32(sizePtr, vec.size, true)
90
+ return E.SUCCESS
91
+ }
92
+
93
+ // Walk an iovec array: calls fn(ptr, len) for each entry; stops early when fn
94
+ // returns false. Returns an errno.
95
+ function iovecs(iovs, n, fn) {
96
+ iovs = u32(iovs)
97
+ if (!inRange(iovs, 8 * n)) return E.FAULT
98
+ for (let i = 0; i < n; i++) {
99
+ const ptr = dv.getUint32(iovs + 8 * i, true)
100
+ const len = dv.getUint32(iovs + 8 * i + 4, true)
101
+ if (!inRange(ptr, len)) return E.FAULT
102
+ if (fn(ptr, len) === false) break
103
+ }
104
+ return E.SUCCESS
105
+ }
106
+
107
+ const wasi = {
108
+ args_sizes_get: (c, s) => putSizes(argv, c, s),
109
+ args_get: (p, b) => putStrings(argv, p, b),
110
+ environ_sizes_get: (c, s) => putSizes(envv, c, s),
111
+ environ_get: (p, b) => putStrings(envv, p, b),
112
+
113
+ // W40: no allocation here (clock.js): vm.wasm calls it thousands of times a second.
114
+ clock_time_get(id, _precision, out) {
115
+ // 0 realtime; 1-3 monotonic, process and thread CPU time: ns since start (as the runner)
116
+ if (id < 0 || id > 3) return E.INVAL
117
+ views()
118
+ out = u32(out)
119
+ if (!inRange(out, 8)) return E.FAULT
120
+ clock.writeNs(dv, out, id)
121
+ return E.SUCCESS
122
+ },
123
+
124
+ fd_close: fd => fs.close(fd),
125
+
126
+ fd_fdstat_get(fd, out) {
127
+ const type = fs.fdType(fd)
128
+ if (type < 0) return E.BADF
129
+ views()
130
+ out = u32(out)
131
+ if (!inRange(out, 24)) return E.FAULT
132
+ u8.fill(0, out, out + 24)
133
+ u8[out] = type
134
+ dv.setBigUint64(out + 8, 0xffffffffffffffffn, true)
135
+ dv.setBigUint64(out + 16, 0xffffffffffffffffn, true)
136
+ return E.SUCCESS
137
+ },
138
+
139
+ fd_fdstat_set_flags: () => E.SUCCESS,
140
+
141
+ fd_filestat_get(fd, out) {
142
+ const st = fs.stat(fd)
143
+ if (!st) return E.BADF
144
+ views()
145
+ out = u32(out)
146
+ if (!inRange(out, 64)) return E.FAULT
147
+ u8.fill(0, out, out + 64)
148
+ u8[out + FILETYPE_OFF] = st.type
149
+ dv.setBigUint64(out + 24, 1n, true) // nlink
150
+ dv.setBigUint64(out + 32, BigInt(st.size), true)
151
+ return E.SUCCESS
152
+ },
153
+
154
+ fd_prestat_get(fd, out) {
155
+ const name = fs.prestatName(fd)
156
+ if (name === null) return E.BADF
157
+ views()
158
+ out = u32(out)
159
+ if (!inRange(out, 8)) return E.FAULT
160
+ dv.setUint32(out, 0, true) // pr_type 0 = directory
161
+ dv.setUint32(out + 4, enc.encode(name).length, true)
162
+ return E.SUCCESS
163
+ },
164
+
165
+ fd_prestat_dir_name(fd, path, len) {
166
+ const name = fs.prestatName(fd)
167
+ if (name === null) return E.BADF
168
+ const b = enc.encode(name)
169
+ if (u32(len) < b.length) return E.INVAL
170
+ views()
171
+ path = u32(path)
172
+ if (!inRange(path, b.length)) return E.FAULT
173
+ u8.set(b, path)
174
+ return E.SUCCESS
175
+ },
176
+
177
+ fd_read(fd, iovs, n, nread) {
178
+ views()
179
+ let total = 0
180
+ let err = E.SUCCESS
181
+ const e = iovecs(iovs, n, (ptr, len) => {
182
+ const got = fs.read(fd, u8.subarray(ptr, ptr + len))
183
+ if (got < 0) {
184
+ err = -got
185
+ return false
186
+ }
187
+ total += got
188
+ return got === len // short read: stop
189
+ })
190
+ if (e) return e
191
+ if (err) return err
192
+ nread = u32(nread)
193
+ if (!inRange(nread, 4)) return E.FAULT
194
+ dv.setUint32(nread, total, true)
195
+ return E.SUCCESS
196
+ },
197
+
198
+ fd_seek(fd, offset, whence, out) {
199
+ const r = fs.seek(fd, offset, whence)
200
+ if (r.errno) return r.errno
201
+ views()
202
+ out = u32(out)
203
+ if (!inRange(out, 8)) return E.FAULT
204
+ dv.setBigUint64(out, BigInt(r.pos), true)
205
+ return E.SUCCESS
206
+ },
207
+
208
+ fd_write(fd, iovs, n, nwritten) {
209
+ if (fd !== 1 && fd !== 2) return fs.fdType(fd) < 0 ? E.BADF : E.NOTCAPABLE
210
+ views()
211
+ let total = 0
212
+ const chunks = []
213
+ const e = iovecs(iovs, n, (ptr, len) => {
214
+ for (let o = 0; o < len; o += CHUNK) {
215
+ // slice(): copy out of shared memory before handing the bytes away.
216
+ chunks.push(u8.slice(ptr + o, ptr + Math.min(len, o + CHUNK)))
217
+ }
218
+ total += len
219
+ })
220
+ if (e) return e
221
+ if (chunks.length === 1) write(fd, chunks[0])
222
+ else if (chunks.length > 1) {
223
+ const all = new Uint8Array(total)
224
+ let o = 0
225
+ for (const c of chunks) {
226
+ all.set(c, o)
227
+ o += c.length
228
+ }
229
+ write(fd, all)
230
+ }
231
+ nwritten = u32(nwritten)
232
+ if (!inRange(nwritten, 4)) return E.FAULT
233
+ dv.setUint32(nwritten, total, true)
234
+ return E.SUCCESS
235
+ },
236
+
237
+ path_open(dirfd, _dirflags, path, pathLen, oflags, _rb, _ri, _fdflags, outFd) {
238
+ views()
239
+ path = u32(path)
240
+ pathLen = u32(pathLen)
241
+ if (pathLen > 4096) return E.INVAL
242
+ if (!inRange(path, pathLen)) return E.FAULT
243
+ let p
244
+ try {
245
+ p = new TextDecoder('utf-8', { fatal: true }).decode(u8.slice(path, path + pathLen))
246
+ } catch {
247
+ return E.INVAL
248
+ }
249
+ const r = fs.open(dirfd, p, oflags)
250
+ if (r.errno) return r.errno
251
+ outFd = u32(outFd)
252
+ if (!inRange(outFd, 4)) return E.FAULT
253
+ dv.setUint32(outFd, r.fd, true)
254
+ return E.SUCCESS
255
+ },
256
+
257
+ // Clock subscriptions only, like the runner: sleep for the smallest timeout and
258
+ // report that one subscription.
259
+ poll_oneoff(subs, events, nsubs, nevents) {
260
+ views()
261
+ subs = u32(subs)
262
+ if (!inRange(subs, 48 * nsubs)) return E.FAULT
263
+ // W40: Numbers, not BigInts (no garbage per call: D-417, D-419); a timeout
264
+ // only needs to be right to the nanoseconds a sleep can tell apart.
265
+ let best = -1
266
+ let bestTimeout = 0
267
+ for (let i = 0; i < nsubs; i++) {
268
+ const s = subs + 48 * i
269
+ if (u8[s + 8] !== 0) continue // not a clock subscription
270
+ const id = dv.getUint32(s + 16, true)
271
+ let timeout = dv.getUint32(s + 28, true) * 4294967296 + dv.getUint32(s + 24, true)
272
+ const flags = dv.getUint16(s + 40, true)
273
+ if (flags & 1) {
274
+ const now = clock.nowNum(id === 0 ? 0 : 1)
275
+ timeout = timeout > now ? timeout - now : 0
276
+ }
277
+ if (best < 0 || timeout < bestTimeout) {
278
+ best = s
279
+ bestTimeout = timeout
280
+ }
281
+ }
282
+ nevents = u32(nevents)
283
+ if (!inRange(nevents, 4)) return E.FAULT
284
+ if (best < 0) {
285
+ dv.setUint32(nevents, 0, true)
286
+ return E.NOTSUP
287
+ }
288
+ // the userdata, before the sleep: the subscription memory is the guest's
289
+ const udLo = dv.getUint32(best, true)
290
+ const udHi = dv.getUint32(best + 4, true)
291
+ const ms = bestTimeout / 1e6
292
+ if (ms > 0) doSleep(ms)
293
+ views()
294
+ events = u32(events)
295
+ if (!inRange(events, 32)) return E.FAULT
296
+ u8.fill(0, events, events + 32)
297
+ dv.setUint32(events, udLo, true) // error 0, type 0 (clock)
298
+ dv.setUint32(events + 4, udHi, true)
299
+ dv.setUint32(nevents, 1, true)
300
+ return E.SUCCESS
301
+ },
302
+
303
+ proc_exit(code) {
304
+ code = code & 0xff
305
+ // First caller wins: later exits (other threads racing) keep the first code.
306
+ if (Atomics.compareExchange(ctl, CTL_EXIT, 0, 1) === 0) {
307
+ Atomics.store(ctl, CTL_EXIT_CODE, code)
308
+ Atomics.notify(ctl, CTL_EXIT)
309
+ onExit?.(code)
310
+ }
311
+ throw new WasiExit(code)
312
+ },
313
+
314
+ sched_yield: () => E.SUCCESS,
315
+
316
+ // vm.wasm seeds the guest kernel's pool from this (W18: the DTB's
317
+ // /chosen/rng-seed). getRandomValues refuses views of shared memory and
318
+ // takes at most 64 KiB per call, so fill a private buffer and copy.
319
+ random_get(ptr, len) {
320
+ views()
321
+ ptr = u32(ptr)
322
+ len = u32(len)
323
+ if (!inRange(ptr, len)) return E.FAULT
324
+ const tmp = new Uint8Array(Math.min(len, 65536))
325
+ for (let off = 0; off < len; off += tmp.length) {
326
+ const n = Math.min(tmp.length, len - off)
327
+ const part = n === tmp.length ? tmp : tmp.subarray(0, n)
328
+ crypto.getRandomValues(part)
329
+ u8.set(part, ptr + off)
330
+ }
331
+ return E.SUCCESS
332
+ },
333
+ }
334
+ return wasi
335
+ }
@@ -0,0 +1,71 @@
1
+ // The 26 `wmd.*` imports of vm.wasm (web/docs/host-interface.md §1.2, §4.1, §8).
2
+ //
3
+ // P-B1 runs the interpreter with no channels, so only ram_map is ever called. The
4
+ // rest are stubs with the values the guest treats as "not available":
5
+ // ram_map → 1 (RAM left as plain wasm memory; return value ignored, main_wasm.c:562)
6
+ // blk_rw → -38 (no swap disk: the browser never passes --swap, so vm.wasm makes no device, D-497)
7
+ // win_init → 1 (address windows off, win.c:226-253); other win_* → 0 (never called)
8
+ // snapshot → -1 ("continuing without it")
9
+ // ctl_attach → -1 (no runner control socket: the guest starts no ctl thread, W3.3)
10
+ // point → -38 (ENOSYS: no mid-container checkpoint; the guest goes on, W3.3); as the
11
+ // weak defaults in guest/vm/src/dev/chan.c
12
+ // jit_load/jit_compile/jit_known → -1, jit_sync/flush/note/yield → no-op when no door is
13
+ // given; the worker passes the P-B2 JIT door (host/jit.js) as `jit`
14
+ // chan_open → -111 (ECONNREFUSED, "nothing listens"; channels are P-B3)
15
+ // chan_send/recv/wake/shutdown/close → -9 (bad handle: no handle was ever issued)
16
+ //
17
+ // Stub calls are counted in the control block (`counters`, layout.js noteHostCall), not
18
+ // reported one by one: --mode jit calls jit_compile thousands of times a second.
19
+ //
20
+ // `overrides` lets later proofs plug in the JIT door and channels without touching
21
+ // the worker wiring: createWmdImports({ jit: {...}, chan: {...} }).
22
+
23
+ import { noteHostCall } from './layout.js'
24
+
25
+ const EBADF = -9
26
+ const ECONNREFUSED = -111
27
+ const ENOSYS = -38
28
+
29
+ export function createWmdImports({ jit = {}, chan = {}, counters, ckpt = null, ram = null } = {}) {
30
+ const note = counters ? name => noteHostCall(counters, name) : () => {}
31
+ return {
32
+ // channels (P-B3)
33
+ chan_open: chan.open || ((_kind, _port) => (note('chan_open'), ECONNREFUSED)),
34
+ chan_send: chan.send || ((_h, _p, _l) => EBADF),
35
+ chan_recv: chan.recv || ((_h, _p, _l, _t) => EBADF),
36
+ chan_wake: chan.wake || (_h => EBADF),
37
+ chan_shutdown: chan.shutdown || (_h => EBADF),
38
+ chan_close: chan.close || (_h => EBADF),
39
+
40
+ // JIT door (P-B2)
41
+ jit_load: jit.load || ((_p, _l) => (note('jit_load'), -1)),
42
+ jit_compile: jit.compile || ((_p, _l) => (note('jit_compile'), -1)),
43
+ jit_sync: jit.sync || (() => note('jit_sync')),
44
+ jit_known: jit.known || (_p => (note('jit_known'), -1)),
45
+ jit_note: jit.note || ((_p, _n) => note('jit_note')),
46
+ jit_flush: jit.flush || (() => note('jit_flush')),
47
+ jit_evict: jit.evict || ((_m, _k, _p, _c) => -1), // D-514: no door, no budget: the guest releases instead
48
+ jit_yield: jit.yield || (() => {}), // W32: worker.js gives a WebKit JIT thread a yield to its event loop
49
+
50
+ snapshot: (_pp, _pl, _hp, _hl) => (note('snapshot'), -1),
51
+ ram_map: (_off, _len) => 1,
52
+ // RAM outside memory 0 (host/ram-mem.js, the flat layout): without `ram` the guest's RAM is in
53
+ // memory 0 and vm.wasm calls none of these (ram_map2 -1: "this host cannot hold RAM outside memory 0")
54
+ ram_map2: ram?.ram_map2 || ((_off, _lowLen, _total) => -1),
55
+ ram_rw: ram?.ram_rw || ((_off, _ptr, _len, _write) => -1),
56
+ ram_cas: ram?.ram_cas || ((_off, _size, _expect, _desired) => 0n),
57
+ blk_rw: (_op, _off, _ptr, _len) => ENOSYS,
58
+
59
+ // W3.3 checkpoints: no control socket and no checkpoint in the browser
60
+ // P-B9: with `ckpt` (host/ckpt-point.js) the guest starts its ctl thread and takes checkpoints
61
+ ctl_attach: ckpt?.ctl_attach || (_wordAddr => -1),
62
+ point: ckpt?.point || ((_kind, _hp, _hl) => ENOSYS),
63
+
64
+ // address windows: off in the browser
65
+ win_init: (_flag, _gen) => 1,
66
+ win_map: () => 0,
67
+ win_reset: () => 0,
68
+ win_unmap: () => 0,
69
+ win_protect: () => 0,
70
+ }
71
+ }