runtime: Unix domain socket natives (step 1 of #119) #428

Closed
opened 2026-10-23 13:17:48 +00:00 by orion · 2 comments
Collaborator

Goal

First step of #119 (Unix domain sockets for std/network, FIRST-PARTY.md gap 21): the runtime can listen on a path,
accept, and connect to a path, behind two new natives, with the existing stream natives (networkReceive,
networkSend, networkShutdown, networkClose) working on the resulting handles unchanged. No std/ source changes
in this step, so the seed that builds the compiler (which imports std/network in
compiler/src/debugger/adapter.trb) still compiles everything.

Design

  • Two natives of std/network, owner network, verdict keep (NATIVES.md): networkListenUnix(path: String, backlog: Int): Int answers the listener's handle or a failure; networkConnectUnix(path: String): Task<Int> answers
    the stream's handle or a failure. Accept is the existing networkAccept on the listener handle, so no third native.
  • Runtime (runtime/io.c, runtime/include/torb_network.h): torb_network_listen_unix and
    torb_network_connect_unix. The socket records learn the family AF_UNIX; the torb_io_address of the core stays
    IP-only, and the path crosses as text and is checked in the hook.
  • Per system, beside torb_io_system_socket / torb_io_system_listen: runtime/os/posix_io.c (sockaddr_un, epoll
    and kqueue unchanged: readiness is family-blind) and runtime/os/iocp.c (Windows 10 1803 and later; AcceptEx and
    ConnectEx get a third slot beside IPv4 and IPv6; if the system refuses ConnectEx for AF_UNIX, the connect runs
    on the core's helper thread as the resolver does, and the commit message or the header says which was needed).
    Written for both; run on Linux in the forge CI like UDP's kqueue/Windows split in NETWORK.md section 4.
  • Path rules: a path longer than the system's sun_path (108 bytes on Linux and Windows, 104 on macOS/BSD, the
    terminating zero included) or containing a NUL byte is invalid argument, never truncated. Linux abstract names
    (a leading @/NUL) are not supported in this step. A path that exists makes listen_unix fail with the system's
    EADDRINUSE (isAddressInUse() in NetworkError); a path nobody listens on makes connect fail with
    ECONNREFUSED/ENOENT (kinds map to ConnectionRefused / Unreachable through the existing error-kind table).
  • The listener removes its socket file when its handle closes (like Go's listeners), best effort, only the file
    this handle bound; nothing else is unlinked. A stream accepted from it has no peer path.
  • Sandbox, fail closed (NETWORK.md section 10 "Where the checks are"): both natives call a new
    torb_sandbox_check_unix(verb, path) in runtime/sandbox.c that stops the script (TORB_SANDBOX_REFUSED) while any
    sandbox is open, with the message The script may not <verb> the Unix socket `<path>`: a sandbox grants no Unix sockets. The grant language for socket paths is a later slice (see #119); unrestricted (torb repl) allows them.
  • Not in this step: any std/ API (step 2 of #119), datagram Unix sockets (SOCK_DGRAM), socket-pair, credentials of
    the peer (SO_PEERCRED), file descriptor passing, abstract names, chmod of the socket file.

Acceptance criteria

  • torb_network_listen_unix and torb_network_connect_unix in runtime/io.c, declared in
    runtime/include/torb_network.h, with hooks in runtime/os/posix_io.c and runtime/os/iocp.c; the existing
    accept, receive, send, shutdown and close work on their handles.
  • torb_sandbox_check_unix in runtime/sandbox.c, called by both natives, and listed in the file's header comment.
  • The two entries in nativeEntries() of compiler/src/backend/c/natives.trb (state Ready);
    runtime/include/torb_natives.h and runtime/machine_natives.c regenerated with torb natives --header, the
    generated-file check green.
  • Two rows (keep, network) in the std/network table of docs/design/NATIVES.md; the audit check stays green.
  • Runtime tests in runtime/tests/io_test.c, run in the forge CI (sh runtime/build.sh job) on Linux: listen on a
    path in a temp directory, connect, accept, exchange bytes both ways, shutdown gives the peer the end of the
    stream; a path that exists is address-in-use; a path nobody listens on is refused; a path over the limit and a
    path with a NUL are invalid argument; the file is gone after the listener's handle closed; a closed handle and a
    TCP handle passed where a Unix one is expected fail with the closed/invalid kind. With a sandbox open (the
    existing sandbox test harness of runtime/tests/) both natives are refused.
  • docs/design/NETWORK.md: a section 4 paragraph "Unix domain sockets, as built (step 1)" with the path rules,
    the unlink-on-close rule and the sandbox rule above; section 10's "Where the checks are" sentence names
    torb_sandbox_check_unix.
  • Tier A green in forge CI (sh tools/gates.sh a); b too, since runtime/ changes.

Seed

Touches the seed: yes (two new natives in the manifest, docs/design/NATIVES.md, CLAUDE.md "Seed and breaking
changes"). This is the first of the two commits of that rule, landed alone as its own PR: "teach the form". The seed
refresh and the publishing of the seed (seed workflow) after it lands are Torben's and the merge gate's, not the
loop's. The second commit is step 2 of #119 and is filed only once the published seed knows the natives.

Commit

One commit: feat(runtime): Unix domain socket natives (at most 100 characters).

Dependencies

None. Not blocked by #426 (socket option natives for TCP streams, step 1 of #120), which is implemented in parallel and
also touches runtime/io.c, runtime/include/torb_network.h, compiler/src/backend/c/natives.trb, the NATIVES.md
table and the two generated natives files: whichever lands second rebases and regenerates torb_natives.h and
machine_natives.c on top of the other. Not a dependency, a note for the rebase.

Source

  • #119 (this step's epic); FIRST-PARTY.md gap 21
  • docs/design/NETWORK.md section 10 ("A new one - Unix domain sockets ... calls one of the checks there, or refuses
    while a sandbox is open: the grant fails closed, never open")
## Goal First step of #119 (Unix domain sockets for `std/network`, FIRST-PARTY.md gap 21): the runtime can listen on a path, accept, and connect to a path, behind two new natives, with the existing stream natives (`networkReceive`, `networkSend`, `networkShutdown`, `networkClose`) working on the resulting handles unchanged. No `std/` source changes in this step, so the seed that builds the compiler (which imports `std/network` in `compiler/src/debugger/adapter.trb`) still compiles everything. ## Design - Two natives of `std/network`, owner `network`, verdict `keep` (NATIVES.md): `networkListenUnix(path: String, backlog: Int): Int` answers the listener's handle or a failure; `networkConnectUnix(path: String): Task<Int>` answers the stream's handle or a failure. Accept is the existing `networkAccept` on the listener handle, so no third native. - Runtime (`runtime/io.c`, `runtime/include/torb_network.h`): `torb_network_listen_unix` and `torb_network_connect_unix`. The socket records learn the family `AF_UNIX`; the `torb_io_address` of the core stays IP-only, and the path crosses as text and is checked in the hook. - Per system, beside `torb_io_system_socket` / `torb_io_system_listen`: `runtime/os/posix_io.c` (`sockaddr_un`, epoll and kqueue unchanged: readiness is family-blind) and `runtime/os/iocp.c` (Windows 10 1803 and later; `AcceptEx` and `ConnectEx` get a third slot beside IPv4 and IPv6; if the system refuses `ConnectEx` for `AF_UNIX`, the connect runs on the core's helper thread as the resolver does, and the commit message or the header says which was needed). Written for both; run on Linux in the forge CI like UDP's kqueue/Windows split in NETWORK.md section 4. - **Path rules**: a path longer than the system's `sun_path` (108 bytes on Linux and Windows, 104 on macOS/BSD, the terminating zero included) or containing a NUL byte is `invalid argument`, never truncated. Linux abstract names (a leading `@`/NUL) are not supported in this step. A path that exists makes `listen_unix` fail with the system's `EADDRINUSE` (`isAddressInUse()` in `NetworkError`); a path nobody listens on makes connect fail with `ECONNREFUSED`/`ENOENT` (kinds map to `ConnectionRefused` / `Unreachable` through the existing error-kind table). - **The listener removes its socket file when its handle closes** (like Go's listeners), best effort, only the file this handle bound; nothing else is unlinked. A stream accepted from it has no peer path. - **Sandbox, fail closed** (NETWORK.md section 10 "Where the checks are"): both natives call a new `torb_sandbox_check_unix(verb, path)` in `runtime/sandbox.c` that stops the script (`TORB_SANDBOX_REFUSED`) while any sandbox is open, with the message ``The script may not <verb> the Unix socket `<path>`: a sandbox grants no Unix sockets``. The grant language for socket paths is a later slice (see #119); `unrestricted` (`torb repl`) allows them. - Not in this step: any `std/` API (step 2 of #119), datagram Unix sockets (`SOCK_DGRAM`), socket-pair, credentials of the peer (`SO_PEERCRED`), file descriptor passing, abstract names, `chmod` of the socket file. ## Acceptance criteria - [ ] `torb_network_listen_unix` and `torb_network_connect_unix` in `runtime/io.c`, declared in `runtime/include/torb_network.h`, with hooks in `runtime/os/posix_io.c` and `runtime/os/iocp.c`; the existing accept, receive, send, shutdown and close work on their handles. - [ ] `torb_sandbox_check_unix` in `runtime/sandbox.c`, called by both natives, and listed in the file's header comment. - [ ] The two entries in `nativeEntries()` of `compiler/src/backend/c/natives.trb` (state `Ready`); `runtime/include/torb_natives.h` and `runtime/machine_natives.c` regenerated with `torb natives --header`, the generated-file check green. - [ ] Two rows (`keep`, `network`) in the `std/network` table of `docs/design/NATIVES.md`; the audit check stays green. - [ ] Runtime tests in `runtime/tests/io_test.c`, run in the forge CI (`sh runtime/build.sh` job) on Linux: listen on a path in a temp directory, connect, accept, exchange bytes both ways, shutdown gives the peer the end of the stream; a path that exists is address-in-use; a path nobody listens on is refused; a path over the limit and a path with a NUL are `invalid argument`; the file is gone after the listener's handle closed; a closed handle and a TCP handle passed where a Unix one is expected fail with the closed/invalid kind. With a sandbox open (the existing sandbox test harness of `runtime/tests/`) both natives are refused. - [ ] `docs/design/NETWORK.md`: a section 4 paragraph "Unix domain sockets, as built (step 1)" with the path rules, the unlink-on-close rule and the sandbox rule above; section 10's "Where the checks are" sentence names `torb_sandbox_check_unix`. - [ ] Tier A green in forge CI (`sh tools/gates.sh a`); `b` too, since `runtime/` changes. ## Seed Touches the seed: **yes** (two new natives in the manifest, `docs/design/NATIVES.md`, CLAUDE.md "Seed and breaking changes"). This is the first of the two commits of that rule, landed alone as its own PR: "teach the form". The seed refresh and the publishing of the seed (`seed` workflow) after it lands are Torben's and the merge gate's, not the loop's. The second commit is step 2 of #119 and is filed only once the published seed knows the natives. ## Commit One commit: `feat(runtime): Unix domain socket natives` (at most 100 characters). ## Dependencies None. Not blocked by #426 (socket option natives for TCP streams, step 1 of #120), which is implemented in parallel and also touches `runtime/io.c`, `runtime/include/torb_network.h`, `compiler/src/backend/c/natives.trb`, the NATIVES.md table and the two generated natives files: whichever lands second rebases and regenerates `torb_natives.h` and `machine_natives.c` on top of the other. Not a dependency, a note for the rebase. ## Source - #119 (this step's epic); [FIRST-PARTY.md gap 21](https://git.torb.dev/torbscript/language/src/branch/main/docs/design/FIRST-PARTY.md#L281) - `docs/design/NETWORK.md` section 10 ("A new one - Unix domain sockets ... calls one of the checks there, or refuses while a sandbox is open: the grant fails closed, never open")
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261023 until 2026-10-23T19:50Z

[loop:torbscript] claim implement by session:loop-torbscript-20261023 until 2026-10-23T19:50Z
Author
Collaborator

[loop:torbscript] implement: both natives, the hooks (POSIX and Windows), torb_sandbox_check_unix, manifest entries, regenerated torb_natives.h / machine_natives.c, NATIVES.md rows, NETWORK.md paragraph and the runtime and sandbox tests are in one commit feat(runtime): Unix domain socket natives, PR #430, CI success on the first run (tier A, tier B, gates, bootstrap linux-x64, generated-file check all green; the Windows, macOS and cross jobs were skipped by the change filter), next: review

Notes for the reviewer:

  • Windows: ConnectEx is not used for AF_UNIX (it needs a bound socket and AF_UNIX has no unnamed address); the connect runs on the core's helper thread. AcceptEx has the third slot. That code is written against the Winsock documentation and was not compiled or run (the Windows jobs were skipped).
  • No torb binary or C compiler was available while writing, so the generated files were produced by a script that mimics torb natives --header; CI's check accepted them.
  • Not touched: std/, seed/; the seed refresh is the second commit of the rule and is not part of this PR.

Tokens: not metered inside this worker session; about 2 context windows of Sonnet work.

[loop:torbscript] implement: both natives, the hooks (POSIX and Windows), `torb_sandbox_check_unix`, manifest entries, regenerated `torb_natives.h` / `machine_natives.c`, NATIVES.md rows, NETWORK.md paragraph and the runtime and sandbox tests are in one commit `feat(runtime): Unix domain socket natives`, PR https://git.torb.dev/torbscript/language/pulls/430, CI success on the first run (tier A, tier B, gates, bootstrap linux-x64, generated-file check all green; the Windows, macOS and cross jobs were skipped by the change filter), next: review Notes for the reviewer: - Windows: `ConnectEx` is not used for `AF_UNIX` (it needs a bound socket and `AF_UNIX` has no unnamed address); the connect runs on the core's helper thread. `AcceptEx` has the third slot. That code is written against the Winsock documentation and was not compiled or run (the Windows jobs were skipped). - No `torb` binary or C compiler was available while writing, so the generated files were produced by a script that mimics `torb natives --header`; CI's check accepted them. - Not touched: `std/`, `seed/`; the seed refresh is the second commit of the rule and is not part of this PR. Tokens: not metered inside this worker session; about 2 context windows of Sonnet work.
argus closed this issue 2026-10-23 14:21:02 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#428
No description provided.