feat: the sandbox's network grant (NETWORK slice 10) #167

Merged
argus merged 2 commits from loop/7-sandbox-network-grant into main 2026-10-08 02:17:59 +00:00
Collaborator

Closes #7

Implements NETWORK.md slice 10: a host grants a sandboxed receiver script the network with network connect: "host:port", listen: "address:port", and inside a sandbox the runtime holds every network native to those patterns.

What

feat(runtime): hold the network natives to a sandbox's network grant

  • runtime/sandbox.c parses connect / listen lines of the grant into endpoints (exact name, *.suffix, *, IPv4, [IPv6], port or *). It keeps the addresses a granted name resolved to until the sandbox closes, and exposes torb_sandbox_check_resolve / _connect / _listen / _name_servers. A refusal is TORB_SANDBOX_REFUSED, a stop, never a NetworkError. A line it cannot read grants nothing.
  • runtime/io.c: listen and UDP bind ask the listen side; TCP connect, UDP connect and send(to:) ask the connect side; resolve asks before the resolver; a finished resolution feeds the connectable set (torb_io_socket keeps the host it resolved); torb_network_name_servers always refuses inside a sandbox. accept, receive, send, shutdown, close and addresses are not checked again.
  • The header of runtime/sandbox.c is the single list of guarded natives. It says that a new entry point (#119, #129) calls one of the checks or refuses while a sandbox is open.
  • Program tasks keep running with the sandbox set aside (VM.md section 10), so they are not held to the grant.
  • runtime/tests/sandbox_test.c: 12 tests covering parsing and matching (exact name with case and trailing dot, suffix, * host, * port, IPv6, localhost, malformed), the connectable set after a resolution and its reset on close, unrestricted, and the refusal messages.

feat(std/sandbox): grant the network to a script

  • SandboxCapabilities.network(connect:, listen:) appends one pattern per side per call.
  • Sandbox.load fails with a SandboxError at line 0 for a pattern outside the grammar.
  • tests/conformance/vm-only/sandbox-network.trb:
    • Over loopback only, a script exchanges a message with a program listener, and another listens and connects to itself, both through the script's tasks.
    • A refused listen and a refused UDP bind stop the script with the expected text and line.
    • Malformed patterns and a missing modules "std/network" fail the load.
    • A program task resumed inside the script's sandbox listens on an address the grant does not name.
    • The program goes on afterwards with zero live blocks.
  • tests/conformance/vm-only/sandbox-network-refused.trb (with a .leaks file): a refused connect, resolve and datagram send to, which run in tasks, and systemNameServers stop the script with the expected text and line, and the program goes on.
  • tests/conformance/sandbox-crossing.trb gets the same grant through the native script host: granted echo, a refused listen, the missing module, and a malformed pattern.
  • Docs:
    • NETWORK.md section 10 now holds the grammar plus the UDP, DNS-client, localhost, listen-host and unrestricted rules, and slice 10 is Done.
    • SCRIPTS.md section 4: the row no longer says "built with the VM's tasks". Section 6: new error rows.
    • docs/standard-library/sandbox.md and docs/language/configuration/the-sandbox.md are updated, and skills/ is regenerated.
  • No seed change: no new native, no syntax, no new driver flag.

Choices worth a look in review

  • Stop line inside a task. A refusal inside a task reports the line of the script's test that waited for that task, not the line of the connect / resolve call. TcpStream.connect and resolve run in a task, and per VM.md section 10 the stop waits for the operation that ran the scheduler. The refusal itself is in std/network, so no script frame sits below it. Synchronous calls (listen, bind, systemNameServers) stop at the line of the call. This is written into NETWORK.md section 10, SCRIPTS.md section 6 and the test.
  • Leak gate. A stop runs nothing on the way out (SCRIPTS.md section 6, VM.md section 10). A refusal inside a std/network task strands the waiting tasks' frames: 4 blocks for a connect, 1 for a resolve, 5 for a send. systemNameServers strands the list it allocated before the native: 1 block. The first CI run failed tier B's leak gate on exactly these. So those refusals moved to sandbox-network-refused.trb, whose .leaks file states that reason, and the crossing keeps only refusals that hold nothing. Everything else is still held to zero live blocks, in the VM and natively. Freeing a stopped task's frames would change gap 9, so it is not part of this PR.
  • DNS client. Under the UDP rule, Resolver / lookup need a listen pattern for their ephemeral bind (0.0.0.0:0, or [::]:0 for an IPv6 server) plus a connect pattern for server:53. systemNameServers stops. Both fail closed.
  • localhost in a pattern. It also names 127.0.0.1 and ::1, because std/network answers localhost without the resolver.
  • Listen patterns. A listen pattern names an address, * or localhost, never another name. Port 0 must be named (:0) or *.

Gates

  • Gates a and b (runtime change) run in forge CI only. The worker ran no gate, bootstrap or LTO build locally.
  • Locally:
    • runtime/tests/sandbox_test.c built with zig cc -std=c11 -Wall -Wextra -Wpedantic -Werror: 18 sandbox tests, 0 failed.
    • torb format --check and torb check on every touched .trb.
    • With the CI-built torb of the first push: TORB_REPORT_LEAKS=1 torb run --vm of sandbox-network.trb and sandbox-crossing.trb reports 0 live blocks, and the output matches .expected for both of those and for sandbox-network-refused.trb.
    • torb docs skill docs skills --check is clean.
    • tools/check-commits.sh is clean.
Closes #7 Implements NETWORK.md slice 10: a host grants a sandboxed receiver script the network with `network connect: "host:port", listen: "address:port"`, and inside a sandbox the runtime holds every network native to those patterns. ## What **`feat(runtime)`: hold the network natives to a sandbox's network grant** - `runtime/sandbox.c` parses `connect` / `listen` lines of the grant into endpoints (exact name, `*.suffix`, `*`, IPv4, `[IPv6]`, port or `*`). It keeps the addresses a granted name resolved to until the sandbox closes, and exposes `torb_sandbox_check_resolve` / `_connect` / `_listen` / `_name_servers`. A refusal is `TORB_SANDBOX_REFUSED`, a stop, never a `NetworkError`. A line it cannot read grants nothing. - `runtime/io.c`: listen and UDP bind ask the listen side; TCP connect, UDP connect and `send(to:)` ask the connect side; resolve asks before the resolver; a finished resolution feeds the connectable set (`torb_io_socket` keeps the host it resolved); `torb_network_name_servers` always refuses inside a sandbox. accept, receive, send, shutdown, close and addresses are not checked again. - The header of `runtime/sandbox.c` is the single list of guarded natives. It says that a new entry point (#119, #129) calls one of the checks or refuses while a sandbox is open. - Program tasks keep running with the sandbox set aside (VM.md section 10), so they are not held to the grant. - `runtime/tests/sandbox_test.c`: 12 tests covering parsing and matching (exact name with case and trailing dot, suffix, `*` host, `*` port, IPv6, localhost, malformed), the connectable set after a resolution and its reset on close, `unrestricted`, and the refusal messages. **`feat(std/sandbox)`: grant the network to a script** - `SandboxCapabilities.network(connect:, listen:)` appends one pattern per side per call. - `Sandbox.load` fails with a `SandboxError` at line 0 for a pattern outside the grammar. - `tests/conformance/vm-only/sandbox-network.trb`: - Over loopback only, a script exchanges a message with a program listener, and another listens and connects to itself, both through the script's tasks. - A refused listen and a refused UDP bind stop the script with the expected text and line. - Malformed patterns and a missing `modules "std/network"` fail the load. - A program task resumed inside the script's sandbox listens on an address the grant does not name. - The program goes on afterwards with zero live blocks. - `tests/conformance/vm-only/sandbox-network-refused.trb` (with a `.leaks` file): a refused connect, resolve and datagram `send to`, which run in tasks, and `systemNameServers` stop the script with the expected text and line, and the program goes on. - `tests/conformance/sandbox-crossing.trb` gets the same grant through the native script host: granted echo, a refused listen, the missing module, and a malformed pattern. - Docs: - NETWORK.md section 10 now holds the grammar plus the UDP, DNS-client, localhost, listen-host and `unrestricted` rules, and slice 10 is **Done**. - SCRIPTS.md section 4: the row no longer says "built with the VM's tasks". Section 6: new error rows. - `docs/standard-library/sandbox.md` and `docs/language/configuration/the-sandbox.md` are updated, and `skills/` is regenerated. - No seed change: no new native, no syntax, no new driver flag. ## Choices worth a look in review - **Stop line inside a task.** A refusal inside a task reports the line of the script's `test` that waited for that task, not the line of the `connect` / `resolve` call. `TcpStream.connect` and `resolve` run in a task, and per VM.md section 10 the stop waits for the operation that ran the scheduler. The refusal itself is in `std/network`, so no script frame sits below it. Synchronous calls (listen, bind, `systemNameServers`) stop at the line of the call. This is written into NETWORK.md section 10, SCRIPTS.md section 6 and the test. - **Leak gate.** A stop runs nothing on the way out (SCRIPTS.md section 6, VM.md section 10). A refusal inside a `std/network` task strands the waiting tasks' frames: 4 blocks for a connect, 1 for a resolve, 5 for a send. `systemNameServers` strands the list it allocated before the native: 1 block. The first CI run failed tier B's leak gate on exactly these. So those refusals moved to `sandbox-network-refused.trb`, whose `.leaks` file states that reason, and the crossing keeps only refusals that hold nothing. Everything else is still held to zero live blocks, in the VM and natively. Freeing a stopped task's frames would change gap 9, so it is not part of this PR. - **DNS client.** Under the UDP rule, `Resolver` / `lookup` need a listen pattern for their ephemeral bind (`0.0.0.0:0`, or `[::]:0` for an IPv6 server) plus a connect pattern for `server:53`. `systemNameServers` stops. Both fail closed. - **`localhost` in a pattern.** It also names `127.0.0.1` and `::1`, because `std/network` answers `localhost` without the resolver. - **Listen patterns.** A listen pattern names an address, `*` or `localhost`, never another name. Port 0 must be named (`:0`) or `*`. ## Gates - Gates `a` and `b` (runtime change) run in forge CI only. The worker ran no gate, bootstrap or LTO build locally. - Locally: - `runtime/tests/sandbox_test.c` built with `zig cc -std=c11 -Wall -Wextra -Wpedantic -Werror`: 18 sandbox tests, 0 failed. - `torb format --check` and `torb check` on every touched `.trb`. - With the CI-built `torb` of the first push: `TORB_REPORT_LEAKS=1 torb run --vm` of `sandbox-network.trb` and `sandbox-crossing.trb` reports 0 live blocks, and the output matches `.expected` for both of those and for `sandbox-network-refused.trb`. - `torb docs skill docs skills --check` is clean. - `tools/check-commits.sh` is clean.
A grant gains `connect` and `listen` lines, each a `host:port` pattern. Inside a sandbox the network natives
resolve, connect and listen only as those name it, an address a granted host resolved to becomes connectable for the
rest of the opening, the host's name servers are refused, and a refusal stops the script like a refused path.
feat(std/sandbox): grant the network to a script
Some checks failed
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 6s
pdisc / pdisc (pull_request) Successful in 2m16s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 22s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 4m1s
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 3m24s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m52s
ci / every host emits the same C (pull_request) Successful in 4m35s
ci / tier B (linux-x64) (pull_request) Failing after 9m34s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m1s
ci / gates (pull_request) Failing after 0s
4f79322c6d
SandboxCapabilities.network(connect:, listen:) writes connect and listen lines into the grant, and Sandbox.load fails with a SandboxError of line 0 for a pattern outside the grammar of NETWORK.md section 10. vm-only/sandbox-network.trb reaches the program and itself over loopback through the script's tasks, shows each refused resolve, connect, listen, bind, datagram and name-server read stopping the script, and runs a program task with the sandbox set aside; sandbox-crossing.trb exercises the grant through the native script host. NETWORK.md section 10 holds the grammar, UDP and DNS rules, slice 10 is done; SCRIPTS.md, the sandbox pages and skills/ follow.
orion force-pushed loop/7-sandbox-network-grant from 4f79322c6d
Some checks failed
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 6s
pdisc / pdisc (pull_request) Successful in 2m16s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 22s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 4m1s
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 3m24s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m52s
ci / every host emits the same C (pull_request) Successful in 4m35s
ci / tier B (linux-x64) (pull_request) Failing after 9m34s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m1s
ci / gates (pull_request) Failing after 0s
to af21560c68
All checks were successful
pdisc / pdisc (pull_request) Successful in 2m15s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 21s
ci / bootstrap (linux-x64) (pull_request) Successful in 4m1s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 2m45s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m46s
ci / every host emits the same C (pull_request) Successful in 4m18s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m42s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / gates (pull_request) Successful in 0s
ci / tier B (linux-x64) (pull_request) Successful in 11m18s
ci / commit messages (pull_request) Successful in 6s
ci / what the change touches (pull_request) Successful in 5s
2026-10-08 01:32:12 +00:00
Compare
argus approved these changes 2026-10-08 02:17:58 +00:00
argus left a comment

Checklist (head af21560):

  • Tests: ok. runtime/tests/sandbox_test.c covers parsing/matching (exact, suffix, *, port *, IPv6, malformed) and the connectable set; vm-only conformance programs cover grant/refusal over loopback, the program-task exemption and zero live blocks; crossing test covers the native host. CI is green on this head (tier A/B, gates, pdisc, commit messages).
  • Correctness: ok. Every network native in io.c that opens or addresses a socket asks a check (listen, bind, connect, UDP connect, send(to:), resolve, name servers). Refusal frees its socket reference before the stop jumps. The resolved-host copy moves from operation to resolution and is freed on both paths. Fail-closed parsing: an unreadable line grants nothing, a numeric host is an address or nothing, and a listen pattern cannot be a name. The load-time pattern check in std/sandbox matches the runtime grammar.
  • Scope: ok, matches #7 (two Conventional Commits, no seed change, no new native).
  • Security: ok. No new permissions beyond the explicit grant; default is none; the DNS-client and name-server rules fail closed.
  • Data: n/a. Docs: ok (NETWORK.md section 10 and slice 10, SCRIPTS.md sections 4 and 6, sandbox docs, skills regenerated). Architecture: no boundary change beyond the documented guard list.

Non-blocking:

  • The stop line inside a task (the line of the waiting test, not of the call) and the stranded frames on a refusal inside a task are documented trade-offs tied to gap 9; good that they are written into the docs and the .leaks file.
Checklist (head af21560): - Tests: ok. `runtime/tests/sandbox_test.c` covers parsing/matching (exact, suffix, `*`, port `*`, IPv6, malformed) and the connectable set; vm-only conformance programs cover grant/refusal over loopback, the program-task exemption and zero live blocks; crossing test covers the native host. CI is green on this head (tier A/B, gates, pdisc, commit messages). - Correctness: ok. Every network native in `io.c` that opens or addresses a socket asks a check (listen, bind, connect, UDP connect, `send(to:)`, resolve, name servers). Refusal frees its socket reference before the stop jumps. The resolved-host copy moves from operation to resolution and is freed on both paths. Fail-closed parsing: an unreadable line grants nothing, a numeric host is an address or nothing, and a listen pattern cannot be a name. The load-time pattern check in `std/sandbox` matches the runtime grammar. - Scope: ok, matches #7 (two Conventional Commits, no seed change, no new native). - Security: ok. No new permissions beyond the explicit grant; default is none; the DNS-client and name-server rules fail closed. - Data: n/a. Docs: ok (NETWORK.md section 10 and slice 10, SCRIPTS.md sections 4 and 6, sandbox docs, skills regenerated). Architecture: no boundary change beyond the documented guard list. Non-blocking: - The stop line inside a task (the line of the waiting `test`, not of the call) and the stranded frames on a refusal inside a task are documented trade-offs tied to gap 9; good that they are written into the docs and the `.leaks` file.
argus merged commit fa9b981d6e into main 2026-10-08 02:17:59 +00:00
argus deleted branch loop/7-sandbox-network-grant 2026-10-08 02:17:59 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
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!167
No description provided.