- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet available"; "no package manager" contradicted [deps]; the deps example would not have compiled (the key IS the module name) - runtime/README: leads with wovm, wo-rt.c demoted to a historical section; dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18 - employee + log-watcher READMEs claimed "does not compile"; both are gates - error catalog: +10 emitted codes incl WO-E250, the only diagnostic the shipped query surface raises; recorded why the sweep rotted - language-surface: group-by parses, then the typechecker refuses it - 00-code-review + 00-link-audit re-run; history kept, not rewritten - 48 dead Rust-era exploration links de-linked rather than re-pointed (their prose names the retired plan by number); successor map -> discarded.md - 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty - releasing.md: dropped a --draft step the workflow never had - new docs/00-doc-audit.md: findings + disposition, incl one row where the audit was wrong and the doc it accused was right - status folders removed: 34 stories flat, status only in frontmatter; 252 links recomputed from resolved paths; board/board-views/structure retaught - story 24 -> in-progress, since frontmatter is now the only truth - new iteration 38: fs mutation verbs + net.connect, the two capability families no iteration owned - new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no source of randomness at all - linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
118 lines
5.7 KiB
Markdown
118 lines
5.7 KiB
Markdown
---
|
||
iteration: "35"
|
||
status: done
|
||
---
|
||
|
||
# Iteration 35 — `net` runtime seams: timeouts, Unix sockets, peer address
|
||
|
||
> Format: `product/story-iteration-template`. Part of
|
||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||
>
|
||
> **LANDED 2026-08-23** (branch `framework-v1b`, with the serving slice
|
||
> riding it): per-call `_dl` deadlines (nil/false = the expected
|
||
> timeout; ids 91–93), `net.listen_unix` (unlink-before-bind, id 94),
|
||
> `net.peer` (id 95). Plane: shard-tick TIMEOUT + expiry sweep +
|
||
> POLL_REMOVE tombstone on uring, extended deadline scan on epoll,
|
||
> fibers POOLED against stale-CQE UAF — the full design in
|
||
> [the review spec](../../superpowers/specs/2026-08-23-net-seams-park-design.md).
|
||
> Proof: all five seams probe-verified on BOTH `WO_IO` backends;
|
||
> `serve_conn` + web-app's fiber-per-connection pattern gate parallel
|
||
> requests, idle eviction, and slow-loris tearing (web-app 41 checks).
|
||
>
|
||
> **Inserted 2026-08-22** — the framework ledger's three 🔧 rows get one
|
||
> owner: "Read/write/idle timeouts — `net` has no timeout surface",
|
||
> "Unix socket binding — `net.listen` is TCP-only", and "Trusted-proxy
|
||
> client IP — needs a peer-address runtime seam". Each is a small
|
||
> builtin-surface addition; the framework knobs built ON them stay
|
||
> framework slices. Off the concurrency chain; no chain item depends on
|
||
> it, but a production-shaped deployment (proxy in front, sockets not
|
||
> ports, slow-client defense) needs all three.
|
||
|
||
## Why this iteration exists
|
||
|
||
The framework cannot defend against a slow client (no read deadline —
|
||
one stalled socket parks a fiber forever), cannot sit behind a
|
||
same-host proxy the idiomatic way (Unix socket beats a loopback port),
|
||
and cannot TRUST `X-Forwarded-For` (parsing is expressible in `.wo`
|
||
today, but verifying the peer actually IS the proxy needs the peer's
|
||
address, which no builtin exposes). All three are runtime seams by
|
||
nature: the information or mechanism lives at the fd level.
|
||
|
||
## Goals
|
||
|
||
- **Deadlines on parked net I/O.** A read/accept/write that would park
|
||
can carry a deadline; expiry resumes the fiber with a distinguishable
|
||
timeout result (nil-or-trap decided by the spec, consistent with the
|
||
stdlib's nil-vs-trap contracts). On serving shards this composes with
|
||
the existing plane — a park is ALREADY a POLL_ADD or TIMEOUT
|
||
submission (arc T4); the seam arms both and takes whichever fires.
|
||
Program mode gets the same surface over blocking syscalls.
|
||
- **Unix-domain listeners and connections** beside the TCP ones — same
|
||
accept/read/write/close builtins afterward (an fd is an fd; only the
|
||
bind/connect shape differs).
|
||
- **The peer's address, readable** — for an accepted connection, enough
|
||
to answer "is this my trusted proxy?" (address + family; port where
|
||
meaningful). The framework's trusted-proxy middleware then becomes a
|
||
pure-`.wo` candidate slice.
|
||
- **The framework knobs are explicitly NOT here** — timeout defaults,
|
||
proxy allowlists, socket-path config all live in framework slices
|
||
that consume these seams.
|
||
|
||
## Acceptance Criteria (draft — the spec refines)
|
||
|
||
- **Given** a fiber reading a socket whose peer sends nothing, **when**
|
||
the declared deadline expires, **then** the fiber resumes with the
|
||
timeout result (never a hang), other fibers having run throughout
|
||
(TID-verified), and the fd is still usable or cleanly closed per the
|
||
spec's stated semantics.
|
||
- **Given** a listener on a Unix socket path, **when** a client
|
||
connects and exchanges bytes, **then** the whole existing net surface
|
||
works unchanged over it, and the log-watcher/web-app gates stay
|
||
byte-identical on TCP.
|
||
- **Given** an accepted connection, **when** the handler asks for the
|
||
peer address, **then** loopback TCP and Unix-socket peers are both
|
||
identifiable, and the answer round-trips into the trusted-proxy
|
||
check's comparison.
|
||
- **Given** the full battery plus a soak with deliberately stalled
|
||
clients, **when** it runs, **then** zero leaked fds and flat RSS —
|
||
timeouts must CLEAN UP, not merely return.
|
||
|
||
## Out Of Scope
|
||
|
||
- Framework policy (default timeout values, proxy allowlist shape,
|
||
keep-alive idle policy) — framework slices on top.
|
||
- TLS, h2c — unchanged owners (proxy; parked behind 23).
|
||
- Connect-side timeouts for outbound clients beyond what the deadline
|
||
seam gives free — no workload asks yet.
|
||
- Cancellation as a general mechanism — iteration 31's
|
||
request/response + timers own actor-level cancellation; this is
|
||
strictly fd-level deadlines.
|
||
|
||
## Info
|
||
|
||
Forks the spec must settle:
|
||
|
||
1. **Timeout result shape**: nil result vs a distinguishable trap —
|
||
must follow the stdlib's existing nil-vs-trap doctrine
|
||
(`07`-series contracts; a timeout is an EXPECTED outcome, which
|
||
argues nil).
|
||
2. **Deadline plumbing on the plane**: one park may need BOTH a
|
||
POLL_ADD and a TIMEOUT in flight (io_uring linked ops vs two
|
||
submissions + first-wins cancel; epoll fallback = the existing
|
||
deadline scan). The park protocol contract
|
||
([`03-concurrency-coroutines.md`](../../plan/oop-vm/03-concurrency-coroutines.md))
|
||
gains the rule.
|
||
3. **Surface shape**: per-call deadline argument vs per-fd setting
|
||
(`net.set_deadline(fd, ms)`); leaning per-call — no hidden fd state,
|
||
matches the no-coloring doctrine.
|
||
4. **Unix-socket path semantics**: unlink-before-bind? stale-socket
|
||
handling on restart (the never-stopping-runtime doctrine says a
|
||
restart must not need manual cleanup).
|
||
|
||
## Proposed Solution
|
||
|
||
Brainstorm → small spec settling the four forks → implement in the
|
||
`time.ticks`/34 shape (builtin ids + park.c deadline arming + contract
|
||
rows + fixtures incl. a stalled-client corpus case). Independent of the
|
||
chain; natural pairing is right before or with iteration 24 (chat wants
|
||
read deadlines for dead-client eviction even before lifecycle timers).
|