Skip to content

refactor(net): restructure the network module into parse/decide/execute phases#128

Open
congwang-mk wants to merge 8 commits into
mainfrom
refactor/net-parse-decide-execute
Open

refactor(net): restructure the network module into parse/decide/execute phases#128
congwang-mk wants to merge 8 commits into
mainfrom
refactor/net-parse-decide-execute

Conversation

@congwang-mk

@congwang-mk congwang-mk commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

The send-family and connect handlers in network.rs re-implement kernel syscall semantics against attacker-controlled memory, and the file had grown to 3151 lines with the key invariants enforced only by convention:

Approach

Restructure network.rs into a network/ module organized around three phases, each enforced by a module boundary rather than review discipline:

  • materialize.rs: parse phase. Every byte-level reader of child-controlled memory. Produces owned values (ChildMsghdr, MaterializedMsg) so later phases structurally cannot re-read child state. One typed msghdr parser replaces the five hand-rolled ones.
  • verdict.rs: decide phase. Pure policy verdicts (destination_verdict) with no I/O and no locks, shared by connect, sendto, and sendmsg. Unit-tested directly.
  • send_engine.rs: execute phase. The only code that performs sends, including the MSG_DONTWAIT-first blocking/defer state machine from fix(net): pass through connected AF_UNIX sendmsg under a destination policy #125. A shared batch_send_step collapses the triplicated sendmmsg block.
  • connect.rs: the IP connect handler decomposed the same way; the decide phase computes a ConnectPlan as data (HTTP-ACL proxy redirect, loopback port remap, or passthrough) via a pure, unit-tested plan_connect_target, and the duplicated v4/v6 bind/getsockname recording collapsed into one helper.
  • unix.rs: the named AF_UNIX gate subsystem shared by connect and send.
  • send.rs: the IP sendto/sendmsg/sendmmsg handlers as thin phase pipelines.
  • rules.rs: --net-allow/--net-deny parsing, DNS resolution, and virtual /etc/hosts composition, with their tests.
  • mod.rs (138 lines): module docs, socket probes, re-exports, and handle_net dispatch.

Behavior-preserving throughout: moves are verbatim wherever possible, and the two rewrites (connect decomposition, batch step extraction) preserve syscall ordering and errnos. Best reviewed commit by commit; each of the seven commits is a self-contained step with tests passing.

Two deliberate micro-changes, called out for review: mmsg_entry_named_unix_path now copies the full 56-byte msghdr instead of the first 12 bytes (matching what the kernel copies per entry; both versions fail closed), and connect(2) now passes the materialized sockaddr length rather than the child-supplied addr_len, which only differs if the sockaddr read came back short.

Test

13 new unit tests that were structurally impossible before the split: ChildMsghdr parsing (short buffer, connected detection, field extraction), destination_verdict (unrestricted, allowlist match, fail-closed on unparseable port), and plan_connect_target (passthrough, remap, v4 redirect, v4-mapped v6 redirect, v6-proxy fail-closed, remap-never-applies-to-redirect).

Full suite green: 457 lib + 284 integration (including the #125 deferred-send and SCM_RIGHTS regression tests) plus the CLI, OCI, and FFI suites, release mode, single-threaded.

🤖 Generated with Claude Code

Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
…y connect and send

Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
…ecute phases

Signed-off-by: Cong Wang <cwang@multikernel.io>
…rules

Signed-off-by: Cong Wang <cwang@multikernel.io>
…odules

Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk congwang-mk force-pushed the refactor/net-parse-decide-execute branch from 32139da to 11ad724 Compare July 6, 2026 23:25
@congwang-mk

Copy link
Copy Markdown
Contributor Author

@dzerik Please take a look. Thanks

@dzerik

dzerik commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Reviewed with a focus on the send-engine, deferred-send, and TOCTOU-sensitive paths. The refactor itself is faithful; separately, the pass surfaced a few real defects in the underlying send/on-behalf logic (pre-existing in main, not introduced by this PR) that are worth addressing around this landing.

Refactor: behavior-preserving

  • The send core (wants_blocking, send_materialized_at, resolve_send, push_until_done, defer_send, complete_batch_entry) is byte-identical to main. batch_send_step reproduces the three old inline sendmmsg blocks exactly — same offset/prior_count/msglen_addr, same Sent/Done/Stop semantics, same msg_len writeback and message-count (not byte-count) return.
  • The named-AF_UNIX sendto still routes through resolve_send rather than an inline blocking sendto.
  • connect.rs keeps "no RESP_CONTINUE under an active destination policy"; verdict table, proxy/remap selection, and orig-dest recording are equivalent.
  • Both deliberate micro-changes are fail-closed; the connect addr_len one also removes a supervisor-side heap OOB read that existed before.

Making "never decide on re-readable child state" structural rather than a comment is a real improvement.

Findings in the underlying logic (worth a follow-up)

1. [high] Uncapped child-controlled sockaddr length → multi-GiB supervisor allocation. The address length reaches read_child_mem uncapped (sendto addr_len = args[5]; sendmsg/sendmmsg msg_namelen; connect), and read_child_mem_vm does vec![0u8; len]. A sandboxed child can call sendto(fd, buf, 1, 0, &sin, 0xFFFFFFFF) — the notify trap fires before the kernel's addrlen > 128 → EINVAL check, so the supervisor allocates ~4 GiB for a ≤128-byte sockaddr. Under RLIMIT_AS / overcommit_memory=2 / low RAM this aborts the whole supervisor (fail-open crash of the monitor). Every other child read is bounded (buf_len 64 MiB, controllen 16 KiB, iovlen 1024, vlen 256); the sockaddr length is the one gap. Fix: cap to size_of::<sockaddr_storage>() (128) at each entry, like its neighbors.

2. [high] Asymmetric named-AF_UNIX datagram gating. With a destination policy active and the unix fs-gate off (net-only sandbox, permissive fs), a named AF_UNIX SOCK_DGRAM sendmsg/sendmmsg falls into the IP path and returns EAFNOSUPPORT (parse_ip_from_sockaddr → None), while the identical sendto Continues and works. Legitimate UDS datagram traffic (e.g. /dev/log) breaks depending only on which syscall the client uses.

3. [medium] Partial-stream send that can't finish reports total failure after committing bytes. On a blocking stream sendmsg whose remainder is completed off-loop, if the deferred work hits its timeout (or a hard error) after offset bytes were already written, the child gets EIO while those bytes were delivered — a retry duplicates them. The in-loop hard-error path already handles this (offset>0 → Ok(offset)); the external timeout drop bypasses it.

One behavior delta this PR does introduce

batch_send_step performs its inline partial send before try_clone(); if the clone fails (fd exhaustion) after ret bytes were already sent, it returns Stop(EIO) without counting the entry, so a retry re-sends those bytes. The old per-entry-dup shape didn't have this exact ordering. Narrow (needs EMFILE at the wrong moment) and fail-open rather than fail-closed — worth a guard.

Lower-severity: a relative sun_path is always refused (/proc/<pid>/root + path concatenated without a separator, cwd ignored); flatten_iovecs splices data past a partially-faulting iovec; msg_iovlen > 1024 is silently truncated and reported as a short success.

Recommendation

The refactor is good and can land on its own merits. Given finding #1 is a supervisor-availability issue reachable from inside the sandbox, it's worth a follow-up (or a fold-in here) that caps the sockaddr length and aligns the AF_UNIX datagram path. Happy to take the sockaddr-cap + AF_UNIX symmetry fix.

@dzerik

dzerik commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Follow-up on the two findings from the review, both opened against main since they predate this refactor:

When applied, both are trivial rebases onto this refactor's network/ split.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants