Skip to content

Commit 6ef16c9

Browse files
NathanFlurryclaude
andcommitted
docs: expand kernel spec with full WasmVM integration and proofing section
- Detail WasmVM current state (TCP/TLS/DNS/poll exists but bypasses kernel) - Specify migration path: driver sockets → kernel socket table - Add new WASI extensions: net_bind, net_listen, net_accept, net_sendto, net_recvfrom - Specify C sysroot patches for bind/listen/accept/sendto/recvfrom - Detail kernel-worker.ts and driver.ts changes - Specify blocking semantics (Atomics.wait for accept/recv) - Specify cooperative signal delivery at syscall boundaries - Add WasmVM-specific test files and C test programs - Add proofing section: implementation review, conformance re-test, expectations update, and PRD update via Ralph Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5d2e621 commit 6ef16c9

1 file changed

Lines changed: 317 additions & 13 deletions

File tree

docs-internal/specs/kernel-networking-consolidation.md

Lines changed: 317 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -305,23 +305,253 @@ Runtimes call kernel DNS before falling through to host adapter.
305305

306306
## Part 4: WasmVM Integration
307307

308-
WasmVM already communicates with the kernel via synchronous RPC (`kernel-worker.ts` with Atomics.wait + SharedArrayBuffer). New kernel network APIs are exposed the same way:
308+
### 4.1 Current State
309+
310+
WasmVM ALREADY has TCP/TLS/DNS/poll support, but it **bypasses the kernel entirely** and goes direct to host:
311+
312+
- **Rust WASI extensions** (`native/wasmvm/crates/wasi-ext/src/lib.rs`): `host_net` module with `net_socket`, `net_connect`, `net_send`, `net_recv`, `net_close`, `net_tls_connect`, `net_getaddrinfo`, `net_setsockopt`, `net_poll`
313+
- **C sysroot patches** (`native/wasmvm/patches/wasi-libc/0008-sockets.patch`): `host_socket.c` with libc implementations of `socket()`, `connect()`, `send()`, `recv()`, `poll()`, `select()`, `getaddrinfo()`, `setsockopt()`
314+
- **Kernel worker** (`packages/wasmvm/src/kernel-worker.ts`): `createHostNetImports()` routes network calls through permission check then RPC
315+
- **Driver** (`packages/wasmvm/src/driver.ts`): `_sockets` Map holds real Node.js `net.Socket` objects, `_nextSocketId` counter, handlers for `netSocket`/`netConnect`/`netSend`/`netRecv`/`netClose`/`netTlsConnect`/`netGetaddrinfo`/`netPoll`
316+
317+
**What's missing in WasmVM:**
318+
- `bind()` — no WASI extension (WasmVM #1: no server sockets)
319+
- `listen()` — no WASI extension (WasmVM #1)
320+
- `accept()` — no WASI extension (WasmVM #1)
321+
- `sendto()`/`recvfrom()` — no UDP datagram support (WasmVM #17)
322+
- Unix domain sockets — no AF_UNIX support (WasmVM #2)
323+
- `setsockopt()` — returns ENOSYS (WasmVM #19)
324+
- Signal handlers — no `sigaction()` (WasmVM #9)
325+
- Socket FDs are NOT kernel FDs — stored in driver's `_sockets` Map, separate from kernel FD table
326+
327+
### 4.2 Migration: Route Existing Sockets Through Kernel
328+
329+
The existing WasmVM network path (`kernel-worker.ts` → RPC → `driver.ts` → real host TCP) must be rerouted through the kernel socket table:
330+
331+
**Step 1: Driver stops managing sockets directly**
332+
333+
Current `driver.ts` handlers (`netSocket`, `netConnect`, etc.) manage `_sockets` Map with real Node.js `Socket` objects. After migration:
334+
- `netSocket` → calls `kernel.socketTable.create()` instead of allocating local ID
335+
- `netConnect` → calls `kernel.socketTable.connect()` which handles loopback vs external routing
336+
- `netSend` → calls `kernel.socketTable.send()`
337+
- `netRecv` → calls `kernel.socketTable.recv()`
338+
- `netClose` → calls `kernel.socketTable.close()`
339+
- `netPoll` → calls `kernel.socketTable.poll()` (unified with pipe poll via `kernel.fdPoll()`)
340+
341+
**Step 2: Unify socket FDs with kernel FD table**
342+
343+
Currently WasmVM socket FDs (`_nextSocketId` in driver.ts) and kernel FDs (`localToKernelFd` map in kernel-worker.ts) are separate number spaces. After migration:
344+
- `kernel.socketTable.create()` returns a kernel FD
345+
- Kernel worker maps local WASM FD → kernel socket FD (same `localToKernelFd` map used for files/pipes)
346+
- `poll()` works across file FDs, pipe FDs, and socket FDs in one call
347+
348+
**Step 3: TLS stays in host adapter**
349+
350+
TLS handshake requires OpenSSL — it can't run in-kernel. The kernel socket table delegates TLS to the host adapter:
351+
- `kernel.socketTable.upgradeTls(socketId, hostname)` → host adapter wraps the host-side socket in TLS
352+
- From the kernel's perspective, the socket is still a kernel socket — TLS is transparent
353+
354+
### 4.3 New WASI Extensions for Server Sockets
355+
356+
Add to `native/wasmvm/crates/wasi-ext/src/lib.rs` under `host_net`:
357+
358+
```rust
359+
// New host imports
360+
fn net_bind(fd: i32, addr_ptr: *const u8, addr_len: u32) -> i32;
361+
fn net_listen(fd: i32, backlog: i32) -> i32;
362+
fn net_accept(fd: i32, ret_fd: *mut i32, ret_addr: *mut u8, ret_addr_len: *mut u32) -> i32;
363+
fn net_sendto(fd: i32, buf: *const u8, len: u32, flags: i32,
364+
addr_ptr: *const u8, addr_len: u32, ret_sent: *mut u32) -> i32;
365+
fn net_recvfrom(fd: i32, buf: *mut u8, len: u32, flags: i32,
366+
ret_addr: *mut u8, ret_addr_len: *mut u32, ret_received: *mut u32) -> i32;
367+
```
368+
369+
Add safe Rust wrappers following the existing pattern (`pub fn bind()`, `pub fn listen()`, etc.).
370+
371+
### 4.4 C Sysroot Patches for Server/UDP/Unix
372+
373+
Extend `native/wasmvm/patches/wasi-libc/0008-sockets.patch` (or create `0009-server-sockets.patch`) to add to `host_socket.c`:
374+
375+
```c
376+
// Server sockets
377+
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
378+
char addr_str[256];
379+
sockaddr_to_string(addr, addrlen, addr_str, sizeof(addr_str));
380+
return __host_net_bind(sockfd, addr_str, strlen(addr_str));
381+
}
382+
383+
int listen(int sockfd, int backlog) {
384+
return __host_net_listen(sockfd, backlog);
385+
}
386+
387+
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
388+
int new_fd = -1;
389+
char remote_addr[256];
390+
uint32_t remote_addr_len = sizeof(remote_addr);
391+
int err = __host_net_accept(sockfd, &new_fd, remote_addr, &remote_addr_len);
392+
if (err != 0) { errno = err; return -1; }
393+
if (addr && addrlen) {
394+
string_to_sockaddr(remote_addr, remote_addr_len, addr, addrlen);
395+
}
396+
return new_fd;
397+
}
398+
399+
// UDP
400+
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
401+
const struct sockaddr *dest_addr, socklen_t addrlen) {
402+
char addr_str[256];
403+
sockaddr_to_string(dest_addr, addrlen, addr_str, sizeof(addr_str));
404+
uint32_t sent = 0;
405+
int err = __host_net_sendto(sockfd, buf, len, flags, addr_str, strlen(addr_str), &sent);
406+
if (err != 0) { errno = err; return -1; }
407+
return (ssize_t)sent;
408+
}
409+
410+
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
411+
struct sockaddr *src_addr, socklen_t *addrlen) {
412+
char addr_str[256];
413+
uint32_t addr_len = sizeof(addr_str), received = 0;
414+
int err = __host_net_recvfrom(sockfd, buf, len, flags, addr_str, &addr_len, &received);
415+
if (err != 0) { errno = err; return -1; }
416+
if (src_addr && addrlen) {
417+
string_to_sockaddr(addr_str, addr_len, src_addr, addrlen);
418+
}
419+
return (ssize_t)received;
420+
}
421+
```
422+
423+
Also add AF_UNIX support in `sockaddr_to_string()` / `string_to_sockaddr()` — serialize `struct sockaddr_un` path to/from string.
424+
425+
### 4.5 Kernel Worker Updates
426+
427+
In `packages/wasmvm/src/kernel-worker.ts`, update `createHostNetImports()`:
428+
429+
```typescript
430+
// Existing imports route through kernel instead of direct RPC:
431+
net_socket: (domain, type, protocol, ret_fd) => {
432+
if (isNetworkBlocked()) return ERRNO_EACCES;
433+
const res = rpcCall('kernelSocketCreate', { domain, type, protocol, pid });
434+
// ...
435+
},
436+
net_connect: (fd, addr_ptr, addr_len) => {
437+
const kernelFd = localToKernelFd.get(fd) ?? fd;
438+
const res = rpcCall('kernelSocketConnect', { socketId: kernelFd, addr });
439+
// ...
440+
},
441+
442+
// New imports:
443+
net_bind: (fd, addr_ptr, addr_len) => {
444+
if (isNetworkBlocked()) return ERRNO_EACCES;
445+
const kernelFd = localToKernelFd.get(fd) ?? fd;
446+
const addr = decodeString(addr_ptr, addr_len);
447+
const res = rpcCall('kernelSocketBind', { socketId: kernelFd, addr });
448+
return res.errno;
449+
},
450+
net_listen: (fd, backlog) => {
451+
if (isNetworkBlocked()) return ERRNO_EACCES;
452+
const kernelFd = localToKernelFd.get(fd) ?? fd;
453+
const res = rpcCall('kernelSocketListen', { socketId: kernelFd, backlog });
454+
return res.errno;
455+
},
456+
net_accept: (fd, ret_fd, ret_addr, ret_addr_len) => {
457+
if (isNetworkBlocked()) return ERRNO_EACCES;
458+
const kernelFd = localToKernelFd.get(fd) ?? fd;
459+
const res = rpcCall('kernelSocketAccept', { socketId: kernelFd });
460+
if (res.errno !== 0) return res.errno;
461+
// Map new kernel socket FD to local FD
462+
const localFd = nextLocalFd++;
463+
localToKernelFd.set(localFd, res.intResult);
464+
writeI32(ret_fd, localFd);
465+
// Write remote address to ret_addr buffer
466+
return 0;
467+
},
468+
net_sendto: (fd, buf, len, flags, addr_ptr, addr_len, ret_sent) => {
469+
// ... permission check, decode, rpcCall('kernelSocketSendTo', ...)
470+
},
471+
net_recvfrom: (fd, buf, len, flags, ret_addr, ret_addr_len, ret_received) => {
472+
// ... rpcCall('kernelSocketRecvFrom', ...), blocks via Atomics.wait
473+
},
474+
```
475+
476+
### 4.6 Driver Updates
477+
478+
In `packages/wasmvm/src/driver.ts`, replace socket handlers with kernel delegation:
479+
480+
```typescript
481+
// Remove: _sockets Map, _nextSocketId counter
482+
// Replace handlers with kernel calls:
483+
484+
case 'kernelSocketCreate':
485+
return kernel.socketTable.create(args.domain, args.type, args.protocol, args.pid);
486+
case 'kernelSocketBind':
487+
return kernel.socketTable.bind(args.socketId, parseAddr(args.addr));
488+
case 'kernelSocketListen':
489+
return kernel.socketTable.listen(args.socketId, args.backlog);
490+
case 'kernelSocketAccept':
491+
return kernel.socketTable.accept(args.socketId); // blocks until connection or EAGAIN
492+
case 'kernelSocketConnect':
493+
return kernel.socketTable.connect(args.socketId, parseAddr(args.addr));
494+
case 'kernelSocketSendTo':
495+
return kernel.socketTable.sendTo(args.socketId, args.data, args.flags, parseAddr(args.addr));
496+
case 'kernelSocketRecvFrom':
497+
return kernel.socketTable.recvFrom(args.socketId, args.maxBytes, args.flags);
498+
```
499+
500+
### 4.7 Blocking Semantics
501+
502+
WasmVM uses `Atomics.wait()` to block the worker thread during syscalls. For blocking socket operations:
503+
504+
- **`accept()`**: If no pending connection, the main thread handler waits for a kernel socket event (connection arrival) before responding. The worker thread stays blocked on `Atomics.wait()`. Timeout: 30s (existing `RPC_WAIT_TIMEOUT_MS`).
505+
- **`recv()`**: If no data in kernel buffer, main thread waits for data or EOF. Same blocking pattern.
506+
- **`connect()` to external**: Main thread creates host TCP connection, waits for connect event, then responds.
507+
- **`connect()` to loopback**: Kernel instantly connects via in-kernel routing — no host wait.
508+
- **Non-blocking mode**: If `O_NONBLOCK` is set on the socket, kernel returns `EAGAIN` immediately instead of blocking. The WASM program uses `poll()` to wait for readiness.
509+
510+
### 4.8 Signal Handler Delivery
511+
512+
WASM cannot be interrupted mid-execution. Signals must be delivered cooperatively:
513+
514+
1. **Registration**: Add `net_sigaction` WASI extension. WASM program calls `sigaction(SIGINT, handler, NULL)`. Kernel worker stores handler function pointer + signal mask in kernel process table entry.
515+
516+
2. **Delivery**: When kernel delivers a signal to a WasmVM process:
517+
- Kernel sets a `pendingSignals` bitmask on the process entry
518+
- At next syscall boundary (any `rpcCall` from worker), kernel worker checks `pendingSignals`
519+
- If signal pending and handler registered: worker invokes the WASM handler function via `instance.exports.__wasi_signal_trampoline(signum)` before returning from the syscall
520+
- If no handler: default behavior (SIGTERM → exit, SIGINT → exit, etc.)
521+
522+
3. **Trampoline**: The C sysroot patch adds a `__wasi_signal_trampoline` export that dispatches to the registered `sigaction` handler. This is called from the JS worker side when a signal is pending.
523+
524+
4. **Limitations**:
525+
- Signals only delivered at syscall boundaries — long-running compute without syscalls won't see signals (WasmVM #10, fundamental WASM limitation)
526+
- `SIGKILL` always terminates immediately (kernel-enforced, no handler invocation)
527+
- `SIGSTOP`/`SIGCONT` handled by kernel process table, not user handlers
528+
529+
### 4.9 WasmVM-Specific Tests
530+
531+
Add to existing test files:
532+
533+
```
534+
packages/wasmvm/test/
535+
net-socket.test.ts # UPDATE: migrate existing tests to use kernel sockets
536+
net-server.test.ts # NEW: bind/listen/accept, loopback server
537+
net-udp.test.ts # NEW: UDP send/recv, message boundaries
538+
net-unix.test.ts # NEW: Unix domain sockets via VFS paths
539+
net-cross-runtime.test.ts # NEW: WasmVM server ↔ Node.js client and vice versa
540+
signal-handler.test.ts # NEW: sigaction registration, cooperative delivery
541+
```
542+
543+
**C test programs** (compiled to WASM):
309544

310545
```
311-
// In kernel-worker.ts, add syscall handlers:
312-
case 'sock_open': return kernel.socketTable.create(domain, type, protocol, pid);
313-
case 'sock_bind': return kernel.socketTable.bind(socketId, addr);
314-
case 'sock_listen': return kernel.socketTable.listen(socketId, backlog);
315-
case 'sock_accept': return kernel.socketTable.accept(socketId);
316-
case 'sock_connect': return kernel.socketTable.connect(socketId, addr);
317-
case 'sock_send': return kernel.socketTable.send(socketId, data, flags);
318-
case 'sock_recv': return kernel.socketTable.recv(socketId, maxBytes, flags);
319-
case 'sock_close': return kernel.socketTable.close(socketId);
320-
case 'sock_setopt': return kernel.socketTable.setsockopt(socketId, level, opt, val);
321-
case 'sock_getopt': return kernel.socketTable.getsockopt(socketId, level, opt);
546+
native/wasmvm/c/programs/
547+
tcp_server.c # bind → listen → accept → recv → send → close
548+
tcp_client.c # socket → connect → send → recv → close
549+
udp_echo.c # socket(SOCK_DGRAM) → bind → recvfrom → sendto
550+
unix_socket.c # socket(AF_UNIX) → bind → listen → accept
551+
signal_handler.c # sigaction(SIGINT, handler) → busy loop → verify handler called
322552
```
323553

324-
WasmVM WASI extensions (`native/wasmvm/crates/wasi-ext/src/lib.rs`) call these via the existing host import mechanism. The C sysroot patches route `socket()`, `bind()`, `listen()`, `accept()`, `connect()`, `send()`, `recv()`, `close()`, `setsockopt()`, `getsockopt()` through these host imports.
554+
These programs are built via `native/wasmvm/c/Makefile` (add to `PATCHED_PROGRAMS` since they use `host_net` imports) and tested via the WasmVM driver in vitest.
325555

326556
---
327557

@@ -472,3 +702,77 @@ it('WasmVM server accepts Node.js client connection', async () => {
472702
11. **DNS cache** (N-10) — nice-to-have
473703
12. **FD table unification** (N-1) — important but risky, do after networking stabilizes
474704
13. **Crypto session cleanup** (N-12) — lowest priority
705+
706+
---
707+
708+
## Part 7: Proofing
709+
710+
After the kernel networking consolidation is implemented, a full audit must be performed before the work is considered complete.
711+
712+
### 7.1 Implementation Review
713+
714+
An adversarial review agent must verify:
715+
716+
1. **Kernel completeness**: Every socket operation (create, bind, listen, accept, connect, send, recv, sendto, recvfrom, close, poll, setsockopt, getsockopt) works in the kernel standalone tests without any runtime attached.
717+
718+
2. **Node.js migration completeness**: No networking code remains in the Node.js bridge that bypasses the kernel. Specifically verify:
719+
- `packages/nodejs/src/driver.ts` has no `servers` Map, no `ownedServerPorts` Set, no `netSockets` Map, no `upgradeSockets` Map
720+
- `packages/nodejs/src/bridge/network.ts` has no `serverRequestListeners` Map, no `activeNetSockets` Map
721+
- `packages/nodejs/src/bridge-handlers.ts` has no socket Maps
722+
- All `http.createServer()` calls route through `kernel.socketTable.listen()`
723+
- All `net.connect()` calls route through `kernel.socketTable.connect()`
724+
- SSRF validation is in the kernel, not the host adapter
725+
726+
3. **WasmVM migration completeness**: No networking code remains in the WasmVM driver that bypasses the kernel. Specifically verify:
727+
- `packages/wasmvm/src/driver.ts` has no `_sockets` Map, no `_nextSocketId` counter
728+
- All `netSocket`/`netConnect`/`netSend`/`netRecv`/`netClose` handlers delegate to kernel
729+
- New handlers exist for `kernelSocketBind`, `kernelSocketListen`, `kernelSocketAccept`, `kernelSocketSendTo`, `kernelSocketRecvFrom`
730+
- Socket FDs are unified with kernel FD table (no separate number space)
731+
- `net_bind`, `net_listen`, `net_accept`, `net_sendto`, `net_recvfrom` WASI extensions exist in `lib.rs`
732+
- C sysroot patches exist for `bind()`, `listen()`, `accept()`, `sendto()`, `recvfrom()`
733+
- `setsockopt()` no longer returns ENOSYS for supported options
734+
735+
4. **Loopback routing**: Verify that a server in one runtime can accept connections from another runtime without any real TCP:
736+
- Node.js `http.createServer()` on port 8080 → WasmVM `curl http://localhost:8080` works
737+
- WasmVM `tcp_server` on port 9090 → Node.js `net.connect(9090)` works
738+
- Neither connection touches the host network stack
739+
740+
5. **Permission enforcement**: Verify deny-by-default for all socket operations through the kernel, for both runtimes.
741+
742+
6. **Signal delivery**: Verify WasmVM signal handlers fire at syscall boundaries for SIGINT, SIGTERM, SIGUSR1.
743+
744+
7. **Resource cleanup**: Verify all sockets, timers, and handles are cleaned up when a process exits, for both runtimes.
745+
746+
### 7.2 Conformance Re-test
747+
748+
After kernel migration:
749+
750+
1. Run the full Node.js conformance suite (`packages/secure-exec/tests/node-conformance/runner.test.ts`)
751+
2. Run the full WasmVM test suite (`packages/wasmvm/test/`)
752+
3. Run the full POSIX conformance suite if socket-related os-tests exist
753+
4. Run the project-matrix suite (`packages/secure-exec/tests/projects/`)
754+
755+
### 7.3 Expectations Update
756+
757+
Tests that were blocked by networking gaps should be re-tested and reclassified:
758+
759+
1. Re-run all 492 FIX-01 (HTTP server) tests — remove expectations for tests that now pass
760+
2. Re-run all 76 dgram tests — remove expectations for tests that now pass
761+
3. Re-run https/tls/net/http2 glob tests — reclassify from `unsupported-module` to specific failure reasons
762+
4. Update `docs-internal/nodejs-compat-roadmap.md` with new pass counts
763+
5. Regenerate conformance report (`scripts/generate-report.ts`)
764+
765+
### 7.4 PRD Update via Ralph
766+
767+
After the review, any remaining gaps, regressions, or incomplete items must be captured as new stories in `scripts/ralph/prd.json`:
768+
769+
1. Load the Ralph skill (`/ralph`)
770+
2. For each gap found during proofing:
771+
- Create a new user story with specific acceptance criteria
772+
- Include the exact test names that are still failing
773+
- Reference the kernel component that needs fixing
774+
3. Stories should be right-sized for one Ralph iteration (one context window)
775+
4. Set priorities sequentially after existing stories
776+
5. Ralph then executes the remaining stories autonomously until all pass
777+
778+
This ensures no gaps are left undocumented and the work converges to completion through automated iteration.

0 commit comments

Comments
 (0)