@secure-exec/core 0.1.1-rc.3 → 0.2.0-rc.2

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 (102) hide show
  1. package/dist/esm-compiler.d.ts +5 -1
  2. package/dist/esm-compiler.js +5 -1
  3. package/dist/fs-helpers.d.ts +1 -1
  4. package/dist/generated/isolate-runtime.d.ts +15 -15
  5. package/dist/generated/isolate-runtime.js +15 -15
  6. package/dist/index.d.ts +24 -5
  7. package/dist/index.js +23 -3
  8. package/dist/isolate-runtime/apply-custom-global-policy.js +3 -3
  9. package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +2 -2
  10. package/dist/isolate-runtime/apply-timing-mitigation-off.js +2 -2
  11. package/dist/isolate-runtime/bridge-attach.js +2 -2
  12. package/dist/isolate-runtime/bridge-initial-globals.js +145 -6
  13. package/dist/isolate-runtime/eval-script-result.js +1 -1
  14. package/dist/isolate-runtime/global-exposure-helpers.js +2 -2
  15. package/dist/isolate-runtime/init-commonjs-module-globals.js +2 -2
  16. package/dist/isolate-runtime/override-process-cwd.js +1 -1
  17. package/dist/isolate-runtime/override-process-env.js +1 -1
  18. package/dist/isolate-runtime/require-setup.js +2868 -494
  19. package/dist/isolate-runtime/set-commonjs-file-globals.js +2 -2
  20. package/dist/isolate-runtime/set-stdin-data.js +1 -1
  21. package/dist/isolate-runtime/setup-dynamic-import.js +78 -19
  22. package/dist/isolate-runtime/setup-fs-facade.js +62 -23
  23. package/dist/kernel/command-registry.d.ts +44 -0
  24. package/dist/kernel/command-registry.js +114 -0
  25. package/dist/kernel/device-layer.d.ts +12 -0
  26. package/dist/kernel/device-layer.js +262 -0
  27. package/dist/kernel/dns-cache.d.ts +29 -0
  28. package/dist/kernel/dns-cache.js +52 -0
  29. package/dist/kernel/fd-table.d.ts +84 -0
  30. package/dist/kernel/fd-table.js +278 -0
  31. package/dist/kernel/file-lock.d.ts +34 -0
  32. package/dist/kernel/file-lock.js +122 -0
  33. package/dist/kernel/host-adapter.d.ts +50 -0
  34. package/dist/kernel/host-adapter.js +8 -0
  35. package/dist/kernel/index.d.ts +36 -0
  36. package/dist/kernel/index.js +34 -0
  37. package/dist/kernel/inode-table.d.ts +43 -0
  38. package/dist/kernel/inode-table.js +85 -0
  39. package/dist/kernel/kernel.d.ts +9 -0
  40. package/dist/kernel/kernel.js +1393 -0
  41. package/dist/kernel/permissions.d.ts +27 -0
  42. package/dist/kernel/permissions.js +118 -0
  43. package/dist/kernel/pipe-manager.d.ts +64 -0
  44. package/dist/kernel/pipe-manager.js +267 -0
  45. package/dist/kernel/proc-layer.d.ts +11 -0
  46. package/dist/kernel/proc-layer.js +501 -0
  47. package/dist/kernel/process-table.d.ts +124 -0
  48. package/dist/kernel/process-table.js +631 -0
  49. package/dist/kernel/pty.d.ts +108 -0
  50. package/dist/kernel/pty.js +541 -0
  51. package/dist/kernel/socket-table.d.ts +312 -0
  52. package/dist/kernel/socket-table.js +1188 -0
  53. package/dist/kernel/timer-table.d.ts +54 -0
  54. package/dist/kernel/timer-table.js +108 -0
  55. package/dist/kernel/types.d.ts +500 -0
  56. package/dist/kernel/types.js +89 -0
  57. package/dist/kernel/user.d.ts +29 -0
  58. package/dist/kernel/user.js +35 -0
  59. package/dist/kernel/vfs.d.ts +54 -0
  60. package/dist/kernel/vfs.js +8 -0
  61. package/dist/kernel/wait.d.ts +45 -0
  62. package/dist/kernel/wait.js +112 -0
  63. package/dist/kernel/wstatus.d.ts +21 -0
  64. package/dist/kernel/wstatus.js +33 -0
  65. package/dist/module-resolver.d.ts +4 -0
  66. package/dist/module-resolver.js +4 -0
  67. package/dist/package-bundler.d.ts +6 -1
  68. package/dist/runtime-driver.d.ts +3 -1
  69. package/dist/shared/bridge-contract.d.ts +349 -22
  70. package/dist/shared/bridge-contract.js +62 -5
  71. package/dist/shared/console-formatter.js +8 -4
  72. package/dist/shared/global-exposure.js +364 -19
  73. package/dist/shared/in-memory-fs.d.ts +33 -11
  74. package/dist/shared/in-memory-fs.js +439 -130
  75. package/dist/shared/permissions.d.ts +4 -6
  76. package/dist/shared/permissions.js +19 -39
  77. package/dist/types.d.ts +8 -159
  78. package/dist/types.js +5 -0
  79. package/package.json +12 -22
  80. package/dist/bridge/active-handles.d.ts +0 -22
  81. package/dist/bridge/active-handles.js +0 -55
  82. package/dist/bridge/child-process.d.ts +0 -99
  83. package/dist/bridge/child-process.js +0 -670
  84. package/dist/bridge/fs.d.ts +0 -281
  85. package/dist/bridge/fs.js +0 -2235
  86. package/dist/bridge/index.d.ts +0 -10
  87. package/dist/bridge/index.js +0 -41
  88. package/dist/bridge/module.d.ts +0 -75
  89. package/dist/bridge/module.js +0 -308
  90. package/dist/bridge/network.d.ts +0 -350
  91. package/dist/bridge/network.js +0 -2050
  92. package/dist/bridge/os.d.ts +0 -13
  93. package/dist/bridge/os.js +0 -256
  94. package/dist/bridge/polyfills.d.ts +0 -2
  95. package/dist/bridge/polyfills.js +0 -11
  96. package/dist/bridge/process.d.ts +0 -89
  97. package/dist/bridge/process.js +0 -1015
  98. package/dist/bridge.js +0 -12496
  99. package/dist/python-runtime.d.ts +0 -16
  100. package/dist/python-runtime.js +0 -45
  101. package/dist/runtime.d.ts +0 -31
  102. package/dist/runtime.js +0 -69
@@ -0,0 +1,1188 @@
1
+ /**
2
+ * Virtual socket table.
3
+ *
4
+ * Manages kernel-level sockets: create, bind, listen, accept, connect,
5
+ * send, recv, close, poll, per-process isolation, and resource limits.
6
+ * Loopback connections are routed entirely in-kernel without touching
7
+ * the host network stack.
8
+ */
9
+ import { WaitQueue } from "./wait.js";
10
+ import { KernelError, SA_RESTART } from "./types.js";
11
+ // ---------------------------------------------------------------------------
12
+ // Socket constants
13
+ // ---------------------------------------------------------------------------
14
+ export const AF_INET = 2;
15
+ export const AF_INET6 = 10;
16
+ export const AF_UNIX = 1;
17
+ export const SOCK_STREAM = 1;
18
+ export const SOCK_DGRAM = 2;
19
+ // Socket option levels
20
+ export const SOL_SOCKET = 1;
21
+ export const IPPROTO_TCP = 6;
22
+ // Socket options (SOL_SOCKET level)
23
+ export const SO_REUSEADDR = 2;
24
+ export const SO_KEEPALIVE = 9;
25
+ export const SO_RCVBUF = 8;
26
+ export const SO_SNDBUF = 7;
27
+ // TCP options (IPPROTO_TCP level)
28
+ export const TCP_NODELAY = 1;
29
+ // Send/recv flags
30
+ export const MSG_PEEK = 0x2;
31
+ export const MSG_DONTWAIT = 0x40;
32
+ export const MSG_NOSIGNAL = 0x4000;
33
+ // UDP limits
34
+ export const MAX_DATAGRAM_SIZE = 65535;
35
+ export const MAX_UDP_QUEUE_DEPTH = 128;
36
+ const EPHEMERAL_PORT_MIN = 49152;
37
+ const EPHEMERAL_PORT_MAX = 65535;
38
+ // File type for socket files in VFS
39
+ export const S_IFSOCK = 0o140000;
40
+ export function isInetAddr(addr) {
41
+ return "host" in addr;
42
+ }
43
+ export function isUnixAddr(addr) {
44
+ return "path" in addr;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Address key helper
48
+ // ---------------------------------------------------------------------------
49
+ /** Canonical string key for a socket address ("host:port" or unix path). */
50
+ export function addrKey(addr) {
51
+ if (isInetAddr(addr))
52
+ return `${addr.host}:${addr.port}`;
53
+ return addr.path;
54
+ }
55
+ /** Canonical string key for a socket option ("level:optname"). */
56
+ export function optKey(level, optname) {
57
+ return `${level}:${optname}`;
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // SocketTable
61
+ // ---------------------------------------------------------------------------
62
+ const DEFAULT_MAX_SOCKETS = 1024;
63
+ export class SocketTable {
64
+ sockets = new Map();
65
+ nextSocketId = 1;
66
+ maxSockets;
67
+ networkCheck;
68
+ hostAdapter;
69
+ vfs;
70
+ getSignalState;
71
+ processExists;
72
+ /** Bound/listening address → socket ID. Used for EADDRINUSE and TCP routing. */
73
+ listeners = new Map();
74
+ /** Bound UDP address → socket ID. Separate from TCP listeners. */
75
+ udpBindings = new Map();
76
+ constructor(options) {
77
+ this.maxSockets = options?.maxSockets ?? DEFAULT_MAX_SOCKETS;
78
+ this.networkCheck = options?.networkCheck;
79
+ this.hostAdapter = options?.hostAdapter;
80
+ this.vfs = options?.vfs;
81
+ this.getSignalState = options?.getSignalState;
82
+ this.processExists = options?.processExists;
83
+ }
84
+ hasHostNetworkAdapter() {
85
+ return this.hostAdapter !== undefined;
86
+ }
87
+ /**
88
+ * Create a new socket owned by the given process.
89
+ * Returns the kernel socket ID.
90
+ */
91
+ create(domain, type, protocol, pid) {
92
+ if (this.sockets.size >= this.maxSockets) {
93
+ throw new KernelError("EMFILE", "too many open sockets");
94
+ }
95
+ if (this.processExists && !this.processExists(pid)) {
96
+ throw new KernelError("ESRCH", `cannot create socket for unknown pid ${pid}`);
97
+ }
98
+ const id = this.nextSocketId++;
99
+ const socket = {
100
+ id,
101
+ domain,
102
+ type,
103
+ protocol,
104
+ state: "created",
105
+ nonBlocking: false,
106
+ options: new Map(),
107
+ pid,
108
+ readBuffer: [],
109
+ readWaiters: new WaitQueue(),
110
+ backlog: [],
111
+ backlogLimit: 0,
112
+ acceptWaiters: new WaitQueue(),
113
+ datagramQueue: [],
114
+ };
115
+ this.sockets.set(id, socket);
116
+ return id;
117
+ }
118
+ /**
119
+ * Get a socket by ID. Returns null if not found.
120
+ */
121
+ get(socketId) {
122
+ return this.sockets.get(socketId) ?? null;
123
+ }
124
+ // -------------------------------------------------------------------
125
+ // Network permission check
126
+ // -------------------------------------------------------------------
127
+ /**
128
+ * Check network permission for an operation. Throws EACCES if the
129
+ * configured policy denies the request or if no policy is set
130
+ * (deny-by-default). Loopback callers should skip this method.
131
+ */
132
+ checkNetworkPermission(op, addr) {
133
+ const request = { op };
134
+ if (addr && isInetAddr(addr)) {
135
+ request.hostname = addr.host;
136
+ }
137
+ if (!this.networkCheck) {
138
+ throw new KernelError("EACCES", `network ${op} denied (no permission policy)`);
139
+ }
140
+ const decision = this.networkCheck(request);
141
+ if (!decision?.allow) {
142
+ const reason = decision?.reason ? `: ${decision.reason}` : "";
143
+ throw new KernelError("EACCES", `network ${op} denied${reason}`);
144
+ }
145
+ }
146
+ // -------------------------------------------------------------------
147
+ // Bind / Listen / Accept
148
+ // -------------------------------------------------------------------
149
+ /**
150
+ * Bind a socket to an address. Transitions to 'bound' and registers
151
+ * the address in the listeners map for port reservation.
152
+ *
153
+ * For Unix domain sockets (UnixAddr), creates a socket file in the
154
+ * VFS if one is configured.
155
+ */
156
+ async bind(socketId, addr, options) {
157
+ const socket = this.requireSocket(socketId);
158
+ if (socket.state !== "created") {
159
+ throw new KernelError("EINVAL", "socket must be in created state to bind");
160
+ }
161
+ const boundAddr = this.assignEphemeralPort(addr, socket);
162
+ // Unix domain sockets: check VFS for existing path
163
+ if (isUnixAddr(boundAddr) && this.vfs) {
164
+ if (await this.vfs.exists(boundAddr.path)) {
165
+ throw new KernelError("EADDRINUSE", `address already in use: ${boundAddr.path}`);
166
+ }
167
+ }
168
+ // UDP uses a separate binding map from TCP
169
+ if (socket.type === SOCK_DGRAM) {
170
+ if (this.isUdpAddrInUse(boundAddr, socket)) {
171
+ throw new KernelError("EADDRINUSE", `address already in use: ${addrKey(boundAddr)}`);
172
+ }
173
+ socket.localAddr = boundAddr;
174
+ socket.state = "bound";
175
+ this.udpBindings.set(addrKey(boundAddr), socketId);
176
+ // Create socket file in VFS for Unix dgram sockets
177
+ if (isUnixAddr(boundAddr) && this.vfs) {
178
+ await this.createSocketFile(boundAddr.path, options?.mode);
179
+ }
180
+ return;
181
+ }
182
+ if (this.isAddrInUse(boundAddr, socket)) {
183
+ throw new KernelError("EADDRINUSE", `address already in use: ${addrKey(boundAddr)}`);
184
+ }
185
+ socket.localAddr = boundAddr;
186
+ socket.state = "bound";
187
+ this.listeners.set(addrKey(boundAddr), socketId);
188
+ // Create socket file in VFS for Unix stream sockets
189
+ if (isUnixAddr(boundAddr) && this.vfs) {
190
+ await this.createSocketFile(boundAddr.path, options?.mode);
191
+ }
192
+ }
193
+ /**
194
+ * Mark a bound socket as listening. The socket must already be bound.
195
+ * Checks network permission before transitioning.
196
+ *
197
+ * When `external` is true and a host adapter is available, creates a
198
+ * real TCP listener via `hostAdapter.tcpListen()` and starts an accept
199
+ * pump that feeds incoming connections into the kernel backlog.
200
+ */
201
+ async listen(socketId, backlogSize = 128, options) {
202
+ const socket = this.requireSocket(socketId);
203
+ if (socket.state !== "bound") {
204
+ throw new KernelError("EINVAL", "socket must be bound before listen");
205
+ }
206
+ socket.backlogLimit = Math.max(0, backlogSize);
207
+ // AF_UNIX listeners stay entirely in-kernel, so host-network policy
208
+ // only applies to inet listeners.
209
+ if (socket.localAddr && isInetAddr(socket.localAddr)) {
210
+ this.checkNetworkPermission("listen", socket.localAddr);
211
+ }
212
+ // External listen — delegate to host adapter
213
+ if (options?.external &&
214
+ this.hostAdapter &&
215
+ socket.localAddr &&
216
+ isInetAddr(socket.localAddr)) {
217
+ const hostListener = await this.hostAdapter.tcpListen(socket.localAddr.host, socket.requestedEphemeralPort ? 0 : socket.localAddr.port);
218
+ socket.hostListener = hostListener;
219
+ socket.external = true;
220
+ // Update port for ephemeral (port 0) bindings
221
+ if (socket.requestedEphemeralPort || socket.localAddr.port === 0) {
222
+ const oldKey = addrKey(socket.localAddr);
223
+ socket.localAddr = {
224
+ host: socket.localAddr.host,
225
+ port: hostListener.port,
226
+ };
227
+ // Re-register in listeners map with actual port
228
+ this.listeners.delete(oldKey);
229
+ this.listeners.set(addrKey(socket.localAddr), socketId);
230
+ }
231
+ socket.state = "listening";
232
+ this.startAcceptPump(socket);
233
+ return;
234
+ }
235
+ socket.state = "listening";
236
+ }
237
+ accept(socketId, options) {
238
+ const socket = this.requireSocket(socketId);
239
+ if (socket.state !== "listening") {
240
+ throw new KernelError("EINVAL", "socket is not listening");
241
+ }
242
+ if (socket.backlog.length === 0 && socket.nonBlocking) {
243
+ throw new KernelError("EAGAIN", "no pending connections on non-blocking socket");
244
+ }
245
+ if (!options?.block) {
246
+ const connId = socket.backlog.shift();
247
+ return connId ?? null;
248
+ }
249
+ return this.acceptBlocking(socket, options.pid);
250
+ }
251
+ /**
252
+ * Find a listening socket that matches the given address.
253
+ * Checks exact match first, then wildcard (0.0.0.0 / ::).
254
+ */
255
+ findListener(addr) {
256
+ if (isInetAddr(addr)) {
257
+ // Exact match
258
+ const sock = this.getListeningSocket(`${addr.host}:${addr.port}`);
259
+ if (sock)
260
+ return sock;
261
+ // Wildcard IPv4
262
+ const wild4 = this.getListeningSocket(`0.0.0.0:${addr.port}`);
263
+ if (wild4)
264
+ return wild4;
265
+ // Wildcard IPv6
266
+ const wild6 = this.getListeningSocket(`:::${addr.port}`);
267
+ if (wild6)
268
+ return wild6;
269
+ return null;
270
+ }
271
+ return this.getListeningSocket(addr.path) ?? null;
272
+ }
273
+ // -------------------------------------------------------------------
274
+ // Shutdown (half-close)
275
+ // -------------------------------------------------------------------
276
+ /**
277
+ * Shut down part of a full-duplex connection.
278
+ * - 'write': peer recv() gets EOF, local send() returns EPIPE
279
+ * - 'read': local recv() returns EOF immediately
280
+ * - 'both': equivalent to shutdown('read') + shutdown('write')
281
+ */
282
+ shutdown(socketId, how) {
283
+ const socket = this.requireSocket(socketId);
284
+ if (socket.state !== "connected" &&
285
+ socket.state !== "write-closed" &&
286
+ socket.state !== "read-closed") {
287
+ throw new KernelError("ENOTCONN", "socket is not connected");
288
+ }
289
+ // Propagate half-close/full-close semantics to real host sockets so
290
+ // external TCP clients observe EOF instead of hanging on response reads.
291
+ socket.hostSocket?.shutdown(how);
292
+ if (how === "both") {
293
+ this.shutdownWrite(socket);
294
+ this.shutdownRead(socket);
295
+ socket.state = "closed";
296
+ return;
297
+ }
298
+ if (how === "write") {
299
+ this.shutdownWrite(socket);
300
+ if (socket.state === "read-closed") {
301
+ socket.state = "closed";
302
+ }
303
+ else {
304
+ socket.state = "write-closed";
305
+ }
306
+ return;
307
+ }
308
+ // how === 'read'
309
+ this.shutdownRead(socket);
310
+ if (socket.state === "write-closed") {
311
+ socket.state = "closed";
312
+ }
313
+ else {
314
+ socket.state = "read-closed";
315
+ }
316
+ }
317
+ /** Signal EOF to the peer by waking their readWaiters. */
318
+ shutdownWrite(socket) {
319
+ if (socket.peerId !== undefined) {
320
+ const peer = this.sockets.get(socket.peerId);
321
+ if (peer) {
322
+ peer.peerWriteClosed = true;
323
+ peer.readWaiters.wakeAll();
324
+ }
325
+ }
326
+ }
327
+ /** Discard unread data and mark the read side as closed. */
328
+ shutdownRead(socket) {
329
+ socket.readBuffer.length = 0;
330
+ socket.readWaiters.wakeAll();
331
+ }
332
+ // -------------------------------------------------------------------
333
+ // Socketpair
334
+ // -------------------------------------------------------------------
335
+ /**
336
+ * Create a pair of connected sockets atomically (for IPC).
337
+ * Returns [socketId1, socketId2]. Both are pre-connected with
338
+ * peerId linking, so data written to one appears in the other's
339
+ * readBuffer via send/recv.
340
+ */
341
+ socketpair(domain, type, protocol, pid) {
342
+ const id1 = this.create(domain, type, protocol, pid);
343
+ const id2 = this.create(domain, type, protocol, pid);
344
+ const sock1 = this.get(id1);
345
+ const sock2 = this.get(id2);
346
+ sock1.peerId = id2;
347
+ sock2.peerId = id1;
348
+ sock1.state = "connected";
349
+ sock2.state = "connected";
350
+ return [id1, id2];
351
+ }
352
+ // -------------------------------------------------------------------
353
+ // Socket options
354
+ // -------------------------------------------------------------------
355
+ /**
356
+ * Set a socket option. Stores the value keyed by "level:optname".
357
+ */
358
+ setsockopt(socketId, level, optname, optval) {
359
+ const socket = this.requireSocket(socketId);
360
+ socket.options.set(optKey(level, optname), optval);
361
+ this.applySocketOptionToHostSocket(socket, level, optname, optval);
362
+ }
363
+ /** Toggle non-blocking behavior for an existing socket. */
364
+ setNonBlocking(socketId, nonBlocking) {
365
+ const socket = this.requireSocket(socketId);
366
+ socket.nonBlocking = nonBlocking;
367
+ }
368
+ /**
369
+ * Get a socket option. Returns the value, or undefined if not set.
370
+ */
371
+ getsockopt(socketId, level, optname) {
372
+ const socket = this.requireSocket(socketId);
373
+ return socket.options.get(optKey(level, optname));
374
+ }
375
+ /** Get the bound/local address for a socket. */
376
+ getLocalAddr(socketId) {
377
+ const socket = this.requireSocket(socketId);
378
+ if (!socket.localAddr) {
379
+ throw new KernelError("EINVAL", "socket has no local address");
380
+ }
381
+ return socket.localAddr;
382
+ }
383
+ /** Get the connected peer address for a socket. */
384
+ getRemoteAddr(socketId) {
385
+ const socket = this.requireSocket(socketId);
386
+ if (!socket.remoteAddr) {
387
+ throw new KernelError("ENOTCONN", "socket is not connected");
388
+ }
389
+ return socket.remoteAddr;
390
+ }
391
+ // -------------------------------------------------------------------
392
+ // Connect (loopback routing)
393
+ // -------------------------------------------------------------------
394
+ /**
395
+ * Connect a socket to a remote address. For loopback (addr matches a
396
+ * kernel listener), creates a paired server-side socket and queues it
397
+ * in the listener's backlog — loopback is always allowed regardless of
398
+ * permission policy. External addresses are checked against the network
399
+ * permission policy and routed through the host adapter.
400
+ */
401
+ async connect(socketId, addr) {
402
+ const socket = this.requireSocket(socketId);
403
+ if (socket.state !== "created" && socket.state !== "bound") {
404
+ throw new KernelError("EINVAL", "socket must be in created or bound state to connect");
405
+ }
406
+ // Mirror POSIX auto-bind behavior so connected client sockets always
407
+ // expose a concrete local address/port to both peers.
408
+ if (!socket.localAddr && isInetAddr(addr)) {
409
+ socket.localAddr = this.assignEphemeralPort({
410
+ host: addr.host.includes(":") ? "::1" : "127.0.0.1",
411
+ port: 0,
412
+ }, socket);
413
+ }
414
+ // Unix domain sockets: check VFS for socket file existence
415
+ if (isUnixAddr(addr) && this.vfs) {
416
+ if (!(await this.vfs.exists(addr.path))) {
417
+ throw new KernelError("ECONNREFUSED", `connection refused: ${addr.path}`);
418
+ }
419
+ }
420
+ const listener = this.findListener(addr);
421
+ if (!listener) {
422
+ if (isUnixAddr(addr)) {
423
+ throw new KernelError("ECONNREFUSED", `connection refused: ${addr.path}`);
424
+ }
425
+ // Check external connections through the deny-by-default network policy.
426
+ this.checkNetworkPermission("connect", addr);
427
+ // Route through host adapter if available
428
+ if (this.hostAdapter && isInetAddr(addr)) {
429
+ if (socket.nonBlocking) {
430
+ socket.state = "connecting";
431
+ socket.remoteAddr = addr;
432
+ this.startExternalConnect(socket, addr);
433
+ throw new KernelError("EINPROGRESS", `connection in progress: ${addrKey(addr)}`);
434
+ }
435
+ const hostSocket = await this.hostAdapter.tcpConnect(addr.host, addr.port);
436
+ socket.state = "connected";
437
+ socket.external = true;
438
+ socket.remoteAddr = addr;
439
+ socket.hostSocket = hostSocket;
440
+ this.applySocketOptionsToHostSocket(socket);
441
+ this.startReadPump(socket);
442
+ return;
443
+ }
444
+ throw new KernelError("ECONNREFUSED", `connection refused: ${addrKey(addr)}`);
445
+ }
446
+ // Loopback — always allowed, no permission check
447
+ if (listener.backlog.length >= listener.backlogLimit) {
448
+ throw new KernelError("ECONNREFUSED", `connection refused: backlog full for ${addrKey(addr)}`);
449
+ }
450
+ // Create server-side socket paired with the client
451
+ const serverSockId = this.create(listener.domain, listener.type, listener.protocol, listener.pid);
452
+ const serverSock = this.get(serverSockId);
453
+ // Set addresses
454
+ socket.remoteAddr = addr;
455
+ serverSock.localAddr = listener.localAddr;
456
+ serverSock.remoteAddr = socket.localAddr;
457
+ // Link peers
458
+ socket.peerId = serverSockId;
459
+ serverSock.peerId = socketId;
460
+ // Transition both to connected
461
+ socket.state = "connected";
462
+ serverSock.state = "connected";
463
+ // Queue server socket in listener's backlog
464
+ listener.backlog.push(serverSockId);
465
+ listener.acceptWaiters.wakeOne();
466
+ }
467
+ // -------------------------------------------------------------------
468
+ // Send / Recv
469
+ // -------------------------------------------------------------------
470
+ /**
471
+ * Send data to the connected peer. Writes to the peer's readBuffer
472
+ * and wakes one pending reader. Returns bytes written.
473
+ *
474
+ * Flags: MSG_NOSIGNAL suppresses SIGPIPE — returns EPIPE error
475
+ * instead of raising SIGPIPE on a broken connection.
476
+ *
477
+ * For external sockets, checks network permission before sending.
478
+ */
479
+ send(socketId, data, flags = 0) {
480
+ const socket = this.requireSocket(socketId);
481
+ const nosignal = (flags & MSG_NOSIGNAL) !== 0;
482
+ if (socket.state === "write-closed" || socket.state === "closed") {
483
+ throw new KernelError("EPIPE", nosignal
484
+ ? "broken pipe (MSG_NOSIGNAL)"
485
+ : "broken pipe: write side shut down");
486
+ }
487
+ if (socket.state !== "connected" && socket.state !== "read-closed") {
488
+ throw new KernelError("ENOTCONN", "socket is not connected");
489
+ }
490
+ // Re-check outbound external writes so pre-existing host sockets still
491
+ // honor deny-by-default network policy.
492
+ if (socket.external) {
493
+ this.checkNetworkPermission("connect", socket.remoteAddr);
494
+ }
495
+ // External socket: write to host socket
496
+ if (socket.external && socket.hostSocket) {
497
+ socket.hostSocket.write(new Uint8Array(data)).catch(() => {
498
+ socket.state = "closed";
499
+ socket.readWaiters.wakeAll();
500
+ });
501
+ return data.length;
502
+ }
503
+ if (socket.peerId === undefined) {
504
+ throw new KernelError("EPIPE", nosignal ? "broken pipe (MSG_NOSIGNAL)" : "broken pipe: peer closed");
505
+ }
506
+ const peer = this.sockets.get(socket.peerId);
507
+ if (!peer) {
508
+ socket.peerId = undefined;
509
+ throw new KernelError("EPIPE", nosignal ? "broken pipe (MSG_NOSIGNAL)" : "broken pipe: peer closed");
510
+ }
511
+ // Enforce SO_RCVBUF on the peer's receive buffer
512
+ const rcvBuf = peer.options.get(optKey(SOL_SOCKET, SO_RCVBUF));
513
+ if (rcvBuf !== undefined) {
514
+ let currentSize = 0;
515
+ for (const chunk of peer.readBuffer)
516
+ currentSize += chunk.length;
517
+ if (currentSize >= rcvBuf) {
518
+ throw new KernelError("EAGAIN", "peer receive buffer full");
519
+ }
520
+ }
521
+ // Copy data into peer's read buffer
522
+ peer.readBuffer.push(new Uint8Array(data));
523
+ peer.readWaiters.wakeOne();
524
+ return data.length;
525
+ }
526
+ recv(socketId, maxBytes, flags = 0, options) {
527
+ const socket = this.requireSocket(socketId);
528
+ const peek = (flags & MSG_PEEK) !== 0;
529
+ const dontwait = (flags & MSG_DONTWAIT) !== 0;
530
+ // read-closed or closed → immediate EOF
531
+ if (socket.state === "read-closed" || socket.state === "closed") {
532
+ return null;
533
+ }
534
+ if (socket.state !== "connected" && socket.state !== "write-closed") {
535
+ throw new KernelError("ENOTCONN", "socket is not connected");
536
+ }
537
+ if (socket.readBuffer.length > 0) {
538
+ if (peek) {
539
+ return this.peekFromBuffer(socket, maxBytes);
540
+ }
541
+ return this.consumeFromBuffer(socket, maxBytes);
542
+ }
543
+ // Buffer empty — check for EOF (peer gone or peer shut down write)
544
+ if (socket.peerId === undefined ||
545
+ !this.sockets.has(socket.peerId) ||
546
+ socket.peerWriteClosed) {
547
+ return null;
548
+ }
549
+ // No data available
550
+ if (socket.nonBlocking || dontwait) {
551
+ throw new KernelError("EAGAIN", socket.nonBlocking
552
+ ? "no data available on non-blocking socket"
553
+ : "no data available (MSG_DONTWAIT)");
554
+ }
555
+ if (options?.block) {
556
+ return this.recvBlocking(socket, maxBytes, flags, options.pid);
557
+ }
558
+ return null;
559
+ }
560
+ // -------------------------------------------------------------------
561
+ // UDP: sendTo / recvFrom
562
+ // -------------------------------------------------------------------
563
+ /**
564
+ * Send a datagram to a specific address (UDP only).
565
+ * For loopback, delivers to the kernel-bound UDP socket. For external
566
+ * addresses, routes through the host adapter (fire-and-forget). Sends
567
+ * to unbound ports are silently dropped (UDP semantics).
568
+ *
569
+ * Returns bytes "sent" (always data.length for UDP — drops are silent).
570
+ */
571
+ sendTo(socketId, data, flags, destAddr) {
572
+ const socket = this.requireSocket(socketId);
573
+ if (socket.type !== SOCK_DGRAM) {
574
+ throw new KernelError("EINVAL", "sendTo requires a datagram socket");
575
+ }
576
+ if (data.length > MAX_DATAGRAM_SIZE) {
577
+ throw new KernelError("EMSGSIZE", "datagram too large (max 65535 bytes)");
578
+ }
579
+ // Loopback routing — find a kernel-bound UDP socket at destAddr
580
+ const target = this.findBoundUdp(destAddr);
581
+ if (target) {
582
+ if (target.datagramQueue.length >= MAX_UDP_QUEUE_DEPTH) {
583
+ return data.length; // Silently drop
584
+ }
585
+ const srcAddr = this.getUdpSourceAddr(socket, destAddr);
586
+ target.datagramQueue.push({ data: new Uint8Array(data), srcAddr });
587
+ target.readWaiters.wakeOne();
588
+ return data.length;
589
+ }
590
+ // External routing via host adapter
591
+ if (socket.hostUdpSocket && this.hostAdapter && isInetAddr(destAddr)) {
592
+ this.checkNetworkPermission("connect", destAddr);
593
+ this.hostAdapter
594
+ .udpSend(socket.hostUdpSocket, new Uint8Array(data), destAddr.host, destAddr.port)
595
+ .catch(() => { });
596
+ return data.length;
597
+ }
598
+ // No loopback target, no host adapter — silently drop (UDP semantics)
599
+ return data.length;
600
+ }
601
+ getUdpSourceAddr(socket, destAddr) {
602
+ if (!socket.localAddr) {
603
+ return isInetAddr(destAddr)
604
+ ? {
605
+ host: destAddr.host.includes(":") ? "::1" : "127.0.0.1",
606
+ port: 0,
607
+ }
608
+ : { path: destAddr.path };
609
+ }
610
+ if (isInetAddr(socket.localAddr) &&
611
+ isInetAddr(destAddr) &&
612
+ (socket.localAddr.host === "0.0.0.0" || socket.localAddr.host === "::")) {
613
+ return {
614
+ host: destAddr.host,
615
+ port: socket.localAddr.port,
616
+ };
617
+ }
618
+ return socket.localAddr;
619
+ }
620
+ /**
621
+ * Receive a datagram from a UDP socket. Returns the datagram and the
622
+ * source address, or null if no datagram is queued.
623
+ *
624
+ * Message boundaries are preserved: each sendTo produces exactly one
625
+ * recvFrom result. If the datagram exceeds maxBytes, excess is
626
+ * discarded (UDP truncation semantics).
627
+ *
628
+ * Flags: MSG_PEEK reads without consuming, MSG_DONTWAIT throws EAGAIN.
629
+ */
630
+ recvFrom(socketId, maxBytes, flags = 0) {
631
+ const socket = this.requireSocket(socketId);
632
+ if (socket.type !== SOCK_DGRAM) {
633
+ throw new KernelError("EINVAL", "recvFrom requires a datagram socket");
634
+ }
635
+ const peek = (flags & MSG_PEEK) !== 0;
636
+ const dontwait = (flags & MSG_DONTWAIT) !== 0;
637
+ if (socket.datagramQueue.length > 0) {
638
+ if (peek) {
639
+ const dgram = socket.datagramQueue[0];
640
+ const data = dgram.data.length <= maxBytes
641
+ ? new Uint8Array(dgram.data)
642
+ : new Uint8Array(dgram.data.subarray(0, maxBytes));
643
+ return { data, srcAddr: dgram.srcAddr };
644
+ }
645
+ const dgram = socket.datagramQueue.shift();
646
+ const data = dgram.data.length <= maxBytes
647
+ ? dgram.data
648
+ : dgram.data.subarray(0, maxBytes);
649
+ return { data, srcAddr: dgram.srcAddr };
650
+ }
651
+ if (dontwait) {
652
+ throw new KernelError("EAGAIN", "no datagram available (MSG_DONTWAIT)");
653
+ }
654
+ return null;
655
+ }
656
+ /**
657
+ * Set up external UDP routing for a bound datagram socket.
658
+ * Creates a host UDP socket via the host adapter and starts a recv
659
+ * pump that feeds incoming datagrams into the kernel datagramQueue.
660
+ */
661
+ async bindExternalUdp(socketId) {
662
+ const socket = this.requireSocket(socketId);
663
+ if (socket.type !== SOCK_DGRAM) {
664
+ throw new KernelError("EINVAL", "bindExternalUdp requires a datagram socket");
665
+ }
666
+ if (socket.state !== "bound") {
667
+ throw new KernelError("EINVAL", "socket must be bound before external UDP bind");
668
+ }
669
+ if (!this.hostAdapter ||
670
+ !socket.localAddr ||
671
+ !isInetAddr(socket.localAddr)) {
672
+ throw new KernelError("EINVAL", "host adapter and inet address required");
673
+ }
674
+ this.checkNetworkPermission("listen", socket.localAddr);
675
+ const hostUdpSocket = await this.hostAdapter.udpBind(socket.localAddr.host, socket.localAddr.port);
676
+ socket.hostUdpSocket = hostUdpSocket;
677
+ socket.external = true;
678
+ this.startUdpRecvPump(socket);
679
+ }
680
+ // -------------------------------------------------------------------
681
+ // Close / Cleanup
682
+ // -------------------------------------------------------------------
683
+ /**
684
+ * Close a socket. The caller must own the socket (per-process isolation).
685
+ * Wakes all pending waiters and frees resources.
686
+ */
687
+ close(socketId, pid) {
688
+ const socket = this.requireSocket(socketId);
689
+ if (socket.pid !== pid) {
690
+ throw new KernelError("EBADF", `socket ${socketId} not owned by pid ${pid}`);
691
+ }
692
+ this.destroySocket(socket);
693
+ }
694
+ /**
695
+ * Poll a socket for readability, writability, and hangup.
696
+ */
697
+ poll(socketId) {
698
+ const socket = this.requireSocket(socketId);
699
+ const closed = socket.state === "closed";
700
+ const readClosed = socket.state === "read-closed";
701
+ const writeClosed = socket.state === "write-closed";
702
+ const pendingAccept = socket.state === "listening" && socket.backlog.length > 0;
703
+ // UDP: readable when datagramQueue has data
704
+ const readable = socket.type === SOCK_DGRAM
705
+ ? socket.datagramQueue.length > 0 || closed
706
+ : socket.readBuffer.length > 0 || pendingAccept || closed || readClosed;
707
+ const writable = socket.state === "connected" ||
708
+ socket.state === "created" ||
709
+ socket.state === "read-closed" ||
710
+ (socket.type === SOCK_DGRAM && socket.state === "bound");
711
+ const hangup = closed || readClosed || writeClosed;
712
+ return { readable, writable, hangup };
713
+ }
714
+ /**
715
+ * Clean up all sockets owned by a process (called on process exit).
716
+ */
717
+ closeAllForProcess(pid) {
718
+ for (const socket of this.sockets.values()) {
719
+ if (socket.pid === pid) {
720
+ this.destroySocket(socket);
721
+ }
722
+ }
723
+ }
724
+ /**
725
+ * Clean up all sockets (called on kernel dispose).
726
+ */
727
+ disposeAll() {
728
+ for (const socket of this.sockets.values()) {
729
+ socket.readWaiters.wakeAll();
730
+ socket.acceptWaiters.wakeAll();
731
+ if (socket.hostSocket) {
732
+ socket.hostSocket.close().catch(() => { });
733
+ }
734
+ if (socket.hostListener) {
735
+ socket.hostListener.close().catch(() => { });
736
+ }
737
+ if (socket.hostUdpSocket) {
738
+ socket.hostUdpSocket.close().catch(() => { });
739
+ }
740
+ }
741
+ this.sockets.clear();
742
+ this.listeners.clear();
743
+ this.udpBindings.clear();
744
+ }
745
+ /** Number of open sockets. */
746
+ get size() {
747
+ return this.sockets.size;
748
+ }
749
+ // -----------------------------------------------------------------------
750
+ // Internal helpers
751
+ // -----------------------------------------------------------------------
752
+ /** Create a socket file in the VFS with S_IFSOCK mode. */
753
+ async createSocketFile(path, mode = 0o755) {
754
+ if (!this.vfs)
755
+ return;
756
+ await this.vfs.writeFile(path, new Uint8Array(0));
757
+ await this.vfs.chmod(path, S_IFSOCK | (mode & 0o777));
758
+ }
759
+ requireSocket(socketId) {
760
+ const socket = this.sockets.get(socketId);
761
+ if (!socket) {
762
+ throw new KernelError("EBADF", `socket ${socketId} not found`);
763
+ }
764
+ return socket;
765
+ }
766
+ /** Wait for an inbound connection, restarting when SA_RESTART applies. */
767
+ async acceptBlocking(socket, pid) {
768
+ while (true) {
769
+ const connId = socket.backlog.shift();
770
+ if (connId !== undefined)
771
+ return connId;
772
+ await this.waitForSocketWake(socket.acceptWaiters, pid, "accept");
773
+ if (socket.state !== "listening") {
774
+ throw new KernelError("EINVAL", "socket is not listening");
775
+ }
776
+ }
777
+ }
778
+ destroySocket(socket) {
779
+ // Tear down queued-but-unaccepted connections with the listener so they
780
+ // cannot leak detached server-side sockets after the listening endpoint closes.
781
+ for (const pendingId of [...socket.backlog]) {
782
+ const pending = this.sockets.get(pendingId);
783
+ if (pending) {
784
+ this.destroySocket(pending);
785
+ }
786
+ }
787
+ socket.backlog.length = 0;
788
+ // Propagate EOF to peer: clear peer link and wake readers
789
+ if (socket.peerId !== undefined) {
790
+ const peer = this.sockets.get(socket.peerId);
791
+ if (peer) {
792
+ peer.peerId = undefined;
793
+ peer.readWaiters.wakeAll();
794
+ }
795
+ }
796
+ // Close host socket for external connections
797
+ if (socket.hostSocket) {
798
+ socket.hostSocket.close().catch(() => { });
799
+ socket.hostSocket = undefined;
800
+ }
801
+ // Close host listener for external-facing server sockets
802
+ if (socket.hostListener) {
803
+ socket.hostListener.close().catch(() => { });
804
+ socket.hostListener = undefined;
805
+ }
806
+ // Close host UDP socket for external datagram sockets
807
+ if (socket.hostUdpSocket) {
808
+ socket.hostUdpSocket.close().catch(() => { });
809
+ socket.hostUdpSocket = undefined;
810
+ }
811
+ // Free listener/binding registration if this socket was bound
812
+ if (socket.localAddr) {
813
+ const key = addrKey(socket.localAddr);
814
+ if (this.listeners.get(key) === socket.id) {
815
+ this.listeners.delete(key);
816
+ }
817
+ if (this.udpBindings.get(key) === socket.id) {
818
+ this.udpBindings.delete(key);
819
+ }
820
+ }
821
+ socket.state = "closed";
822
+ socket.readBuffer.length = 0;
823
+ socket.datagramQueue.length = 0;
824
+ socket.readWaiters.wakeAll();
825
+ socket.acceptWaiters.wakeAll();
826
+ this.sockets.delete(socket.id);
827
+ }
828
+ /** Background pump: reads from host socket and feeds kernel readBuffer. */
829
+ startReadPump(socket) {
830
+ if (!socket.hostSocket)
831
+ return;
832
+ const hostSocket = socket.hostSocket;
833
+ const pump = async () => {
834
+ try {
835
+ while (socket.state !== "closed" && socket.hostSocket === hostSocket) {
836
+ const data = await hostSocket.read();
837
+ if (data === null) {
838
+ // EOF from host
839
+ socket.peerWriteClosed = true;
840
+ socket.readWaiters.wakeAll();
841
+ break;
842
+ }
843
+ socket.readBuffer.push(data);
844
+ socket.readWaiters.wakeOne();
845
+ }
846
+ }
847
+ catch {
848
+ // Connection error — mark as closed
849
+ if (socket.state !== "closed") {
850
+ socket.peerWriteClosed = true;
851
+ socket.readWaiters.wakeAll();
852
+ }
853
+ }
854
+ };
855
+ pump();
856
+ }
857
+ /** Complete a non-blocking external connect in the background. */
858
+ startExternalConnect(socket, addr) {
859
+ if (!this.hostAdapter)
860
+ return;
861
+ this.hostAdapter
862
+ .tcpConnect(addr.host, addr.port)
863
+ .then((hostSocket) => {
864
+ const current = this.sockets.get(socket.id);
865
+ if (!current || current !== socket || current.state === "closed") {
866
+ hostSocket.close().catch(() => { });
867
+ return;
868
+ }
869
+ current.state = "connected";
870
+ current.external = true;
871
+ current.remoteAddr = addr;
872
+ current.hostSocket = hostSocket;
873
+ this.applySocketOptionsToHostSocket(current);
874
+ this.startReadPump(current);
875
+ })
876
+ .catch(() => {
877
+ const current = this.sockets.get(socket.id);
878
+ if (!current || current !== socket || current.state === "closed") {
879
+ return;
880
+ }
881
+ current.state = "created";
882
+ current.remoteAddr = undefined;
883
+ current.external = false;
884
+ current.hostSocket = undefined;
885
+ current.readWaiters.wakeAll();
886
+ });
887
+ }
888
+ /** Background pump: accepts incoming connections from host listener and feeds kernel backlog. */
889
+ startAcceptPump(socket) {
890
+ if (!socket.hostListener)
891
+ return;
892
+ const hostListener = socket.hostListener;
893
+ const pump = async () => {
894
+ try {
895
+ while (socket.state === "listening" &&
896
+ socket.hostListener === hostListener) {
897
+ const hostSocket = await hostListener.accept();
898
+ if (socket.backlog.length >= socket.backlogLimit) {
899
+ hostSocket.close().catch(() => { });
900
+ continue;
901
+ }
902
+ // Create a kernel socket for this incoming connection
903
+ const connId = this.create(socket.domain, socket.type, socket.protocol, socket.pid);
904
+ const connSock = this.get(connId);
905
+ connSock.state = "connected";
906
+ connSock.external = true;
907
+ connSock.hostSocket = hostSocket;
908
+ connSock.localAddr = socket.localAddr;
909
+ this.applySocketOptionsToHostSocket(connSock);
910
+ // Start read pump for the accepted socket
911
+ this.startReadPump(connSock);
912
+ // Queue in listener's backlog
913
+ socket.backlog.push(connId);
914
+ socket.acceptWaiters.wakeOne();
915
+ }
916
+ }
917
+ catch {
918
+ // Listener closed or error — stop pump
919
+ }
920
+ };
921
+ pump();
922
+ }
923
+ /** Look up a listening socket by exact address key. */
924
+ getListeningSocket(key) {
925
+ const id = this.listeners.get(key);
926
+ if (id === undefined)
927
+ return null;
928
+ const sock = this.sockets.get(id);
929
+ if (!sock || sock.state !== "listening")
930
+ return null;
931
+ return sock;
932
+ }
933
+ /** Replay stored socket options onto a host-backed connection. */
934
+ applySocketOptionsToHostSocket(socket) {
935
+ for (const [key, value] of socket.options.entries()) {
936
+ const [level, optname] = key.split(":").map(Number);
937
+ if (Number.isNaN(level) || Number.isNaN(optname))
938
+ continue;
939
+ this.applySocketOptionToHostSocket(socket, level, optname, value);
940
+ }
941
+ }
942
+ /** Best-effort option forwarding for host-backed sockets. */
943
+ applySocketOptionToHostSocket(socket, level, optname, optval) {
944
+ if (!socket.external || !socket.hostSocket) {
945
+ return;
946
+ }
947
+ try {
948
+ socket.hostSocket.setOption(level, optname, optval);
949
+ }
950
+ catch {
951
+ // Host adapters may not support every kernel-tracked option.
952
+ }
953
+ }
954
+ /** Peek up to maxBytes from a socket's readBuffer without consuming. */
955
+ peekFromBuffer(socket, maxBytes) {
956
+ const chunks = [];
957
+ let totalLen = 0;
958
+ for (const chunk of socket.readBuffer) {
959
+ if (totalLen >= maxBytes)
960
+ break;
961
+ const remaining = maxBytes - totalLen;
962
+ if (chunk.length <= remaining) {
963
+ chunks.push(chunk);
964
+ totalLen += chunk.length;
965
+ }
966
+ else {
967
+ chunks.push(chunk.subarray(0, remaining));
968
+ totalLen += remaining;
969
+ }
970
+ }
971
+ if (chunks.length === 1)
972
+ return new Uint8Array(chunks[0]);
973
+ const result = new Uint8Array(totalLen);
974
+ let offset = 0;
975
+ for (const c of chunks) {
976
+ result.set(c, offset);
977
+ offset += c.length;
978
+ }
979
+ return result;
980
+ }
981
+ /** Consume up to maxBytes from a socket's readBuffer. */
982
+ consumeFromBuffer(socket, maxBytes) {
983
+ const chunks = [];
984
+ let totalLen = 0;
985
+ while (socket.readBuffer.length > 0 && totalLen < maxBytes) {
986
+ const chunk = socket.readBuffer[0];
987
+ const remaining = maxBytes - totalLen;
988
+ if (chunk.length <= remaining) {
989
+ chunks.push(chunk);
990
+ totalLen += chunk.length;
991
+ socket.readBuffer.shift();
992
+ }
993
+ else {
994
+ chunks.push(chunk.subarray(0, remaining));
995
+ socket.readBuffer[0] = chunk.subarray(remaining);
996
+ totalLen += remaining;
997
+ }
998
+ }
999
+ if (chunks.length === 1)
1000
+ return chunks[0];
1001
+ const result = new Uint8Array(totalLen);
1002
+ let offset = 0;
1003
+ for (const c of chunks) {
1004
+ result.set(c, offset);
1005
+ offset += c.length;
1006
+ }
1007
+ return result;
1008
+ }
1009
+ /** Wait for readable data, restarting when SA_RESTART applies. */
1010
+ async recvBlocking(socket, maxBytes, flags, pid) {
1011
+ while (true) {
1012
+ const result = this.recv(socket.id, maxBytes, flags);
1013
+ if (result !== null)
1014
+ return result;
1015
+ if (!this.canBlockForRecv(socket))
1016
+ return null;
1017
+ await this.waitForSocketWake(socket.readWaiters, pid, "recv");
1018
+ }
1019
+ }
1020
+ /** Check whether recv() could still yield data later instead of EOF. */
1021
+ canBlockForRecv(socket) {
1022
+ if (socket.state === "read-closed" || socket.state === "closed") {
1023
+ return false;
1024
+ }
1025
+ if (socket.readBuffer.length > 0) {
1026
+ return false;
1027
+ }
1028
+ if (socket.external) {
1029
+ return !socket.peerWriteClosed;
1030
+ }
1031
+ return (socket.peerId !== undefined &&
1032
+ this.sockets.has(socket.peerId) &&
1033
+ !socket.peerWriteClosed);
1034
+ }
1035
+ /** Wait for socket readiness or an interrupting signal. */
1036
+ async waitForSocketWake(waiters, pid, op) {
1037
+ const signalState = this.getSignalState?.(pid);
1038
+ if (!signalState) {
1039
+ const handle = waiters.enqueue();
1040
+ await handle.wait();
1041
+ waiters.remove(handle);
1042
+ return;
1043
+ }
1044
+ const startSeq = signalState.deliverySeq;
1045
+ const socketHandle = waiters.enqueue();
1046
+ const signalHandle = signalState.signalWaiters.enqueue();
1047
+ if (signalState.deliverySeq !== startSeq) {
1048
+ signalHandle.wake();
1049
+ }
1050
+ try {
1051
+ const winner = await Promise.race([
1052
+ socketHandle.wait().then(() => "socket"),
1053
+ signalHandle.wait().then(() => "signal"),
1054
+ ]);
1055
+ if (winner === "signal" && signalState.deliverySeq !== startSeq) {
1056
+ if ((signalState.lastDeliveredFlags & SA_RESTART) !== 0) {
1057
+ return;
1058
+ }
1059
+ throw new KernelError("EINTR", `${op} interrupted by signal ${signalState.lastDeliveredSignal ?? "unknown"}`);
1060
+ }
1061
+ }
1062
+ finally {
1063
+ waiters.remove(socketHandle);
1064
+ signalState.signalWaiters.remove(signalHandle);
1065
+ }
1066
+ }
1067
+ /** Find a bound UDP socket that matches the given address (exact + wildcard). */
1068
+ findBoundUdp(addr) {
1069
+ if (isInetAddr(addr)) {
1070
+ const sock = this.getBoundUdpSocket(`${addr.host}:${addr.port}`);
1071
+ if (sock)
1072
+ return sock;
1073
+ const wild4 = this.getBoundUdpSocket(`0.0.0.0:${addr.port}`);
1074
+ if (wild4)
1075
+ return wild4;
1076
+ const wild6 = this.getBoundUdpSocket(`:::${addr.port}`);
1077
+ if (wild6)
1078
+ return wild6;
1079
+ return null;
1080
+ }
1081
+ return this.getBoundUdpSocket(addr.path) ?? null;
1082
+ }
1083
+ /** Look up a bound UDP socket by exact address key. */
1084
+ getBoundUdpSocket(key) {
1085
+ const id = this.udpBindings.get(key);
1086
+ if (id === undefined)
1087
+ return null;
1088
+ const sock = this.sockets.get(id);
1089
+ if (!sock || sock.type !== SOCK_DGRAM)
1090
+ return null;
1091
+ return sock;
1092
+ }
1093
+ /** Check if a UDP address conflicts with an existing UDP binding. */
1094
+ isUdpAddrInUse(addr, socket) {
1095
+ if (!isInetAddr(addr)) {
1096
+ return this.udpBindings.has(addr.path);
1097
+ }
1098
+ if (socket.options.get(optKey(SOL_SOCKET, SO_REUSEADDR)) === 1)
1099
+ return false;
1100
+ if (this.udpBindings.has(addrKey(addr)))
1101
+ return true;
1102
+ const isWildcard = addr.host === "0.0.0.0" || addr.host === "::";
1103
+ for (const existingId of this.udpBindings.values()) {
1104
+ const existing = this.sockets.get(existingId);
1105
+ if (!existing?.localAddr || !isInetAddr(existing.localAddr))
1106
+ continue;
1107
+ if (existing.localAddr.port !== addr.port)
1108
+ continue;
1109
+ const existingIsWildcard = existing.localAddr.host === "0.0.0.0" ||
1110
+ existing.localAddr.host === "::";
1111
+ if (isWildcard || existingIsWildcard)
1112
+ return true;
1113
+ }
1114
+ return false;
1115
+ }
1116
+ /** Background pump: receives datagrams from host UDP socket and feeds kernel datagramQueue. */
1117
+ startUdpRecvPump(socket) {
1118
+ if (!socket.hostUdpSocket)
1119
+ return;
1120
+ const hostUdpSocket = socket.hostUdpSocket;
1121
+ const pump = async () => {
1122
+ try {
1123
+ while (socket.state !== "closed" &&
1124
+ socket.hostUdpSocket === hostUdpSocket) {
1125
+ const result = await hostUdpSocket.recv();
1126
+ if (socket.datagramQueue.length < MAX_UDP_QUEUE_DEPTH) {
1127
+ socket.datagramQueue.push({
1128
+ data: result.data,
1129
+ srcAddr: {
1130
+ host: result.remoteAddr.host,
1131
+ port: result.remoteAddr.port,
1132
+ },
1133
+ });
1134
+ socket.readWaiters.wakeOne();
1135
+ }
1136
+ }
1137
+ }
1138
+ catch {
1139
+ // Socket closed or error — stop pump
1140
+ }
1141
+ };
1142
+ pump();
1143
+ }
1144
+ /** Check if an address conflicts with an existing TCP binding. */
1145
+ isAddrInUse(addr, socket) {
1146
+ if (!isInetAddr(addr)) {
1147
+ return this.listeners.has(addr.path);
1148
+ }
1149
+ // SO_REUSEADDR on the new socket skips the check
1150
+ if (socket.options.get(optKey(SOL_SOCKET, SO_REUSEADDR)) === 1)
1151
+ return false;
1152
+ // Exact match
1153
+ if (this.listeners.has(addrKey(addr)))
1154
+ return true;
1155
+ // Wildcard overlap: same port, either side is wildcard
1156
+ const isWildcard = addr.host === "0.0.0.0" || addr.host === "::";
1157
+ for (const existingId of this.listeners.values()) {
1158
+ const existing = this.sockets.get(existingId);
1159
+ if (!existing?.localAddr || !isInetAddr(existing.localAddr))
1160
+ continue;
1161
+ if (existing.localAddr.port !== addr.port)
1162
+ continue;
1163
+ const existingIsWildcard = existing.localAddr.host === "0.0.0.0" ||
1164
+ existing.localAddr.host === "::";
1165
+ if (isWildcard || existingIsWildcard)
1166
+ return true;
1167
+ }
1168
+ return false;
1169
+ }
1170
+ /** Assign a kernel-managed ephemeral port for bind(port=0). */
1171
+ assignEphemeralPort(addr, socket) {
1172
+ if (!isInetAddr(addr) || addr.port !== 0) {
1173
+ socket.requestedEphemeralPort = false;
1174
+ return addr;
1175
+ }
1176
+ socket.requestedEphemeralPort = true;
1177
+ for (let port = EPHEMERAL_PORT_MIN; port <= EPHEMERAL_PORT_MAX; port++) {
1178
+ const candidate = { host: addr.host, port };
1179
+ const inUse = socket.type === SOCK_DGRAM
1180
+ ? this.isUdpAddrInUse(candidate, socket)
1181
+ : this.isAddrInUse(candidate, socket);
1182
+ if (!inUse) {
1183
+ return candidate;
1184
+ }
1185
+ }
1186
+ throw new KernelError("EADDRINUSE", "no ephemeral ports available");
1187
+ }
1188
+ }