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