ExSandbox.Egress.Pasta (ExSandbox v1.0.1)

Copy Markdown View Source

Finds the process that actually holds a sandbox's network namespace (005 T060a3, contracts/egress.md).

Why this is a module and not two lines at the call site

pasta -P <pidfile> records pasta's own host-side pid. The tenant runs in a child. Measured in the isolation container:

pidfile pid = 10 -> ns net:[4026534462]   <- the HOST namespace
tenant  pid = 11 -> ns net:[4026534599]   <- the sandbox namespace
host  ns    =       net:[4026534462]

Using the pidfile pid to install the redirect is not an error that surfaces. nsenter -t 10 -n nft add rule … succeeds: it installs the sandbox's NAT rule into the host's own namespace. The tenant is left entirely unpoliced, the host acquires a stray rule that redirects its outbound TCP, and nothing in the launch reports a problem. Every denial check still passes, because the tenant reaches nothing it is checked against for unrelated reasons.

That is the single most dangerous mistake available on this path, and it is one identifier long. It gets a module, a name, and this comment.

How the holder is identified

By comparing namespace inodes/proc/<pid>/ns/net — against our own. Not by process name, not by tree position, not by assuming the first child.

The inode is the only check a merely-configured-looking namespace cannot fool: two processes are in the same network namespace exactly when the symlinks match. A name-based or position-based check answers a question about process bookkeeping when the question is about kernel identity.

⚠️ A holder whose namespace equals ours is not a holder. find/2 refuses rather than returning it, because that value's only use is to be handed to nsenter, and handing it the host namespace is precisely the catastrophe above. There is no caller for whom "the host namespace" is a useful answer.

Why finding it is a poll rather than a read

pasta forks the tenant after it finishes configuring the namespace. A single check immediately after launch finds no child at all and reports what reads as an architectural refusal — "nothing entered a different namespace" — when the truth is only that nothing has yet. Measured: at t+2s the child did not exist; it appeared shortly after.

Summary

Types

Why the namespace holder could not be identified.

Functions

The pid of the process inside pasta's namespace, or a refusal.

One pass over pasta_pid's children, without waiting.

Types

refusal()

@type refusal() :: :no_holder | :unreadable_self

Why the namespace holder could not be identified.

Functions

find(pasta_pid, opts \\ [])

@spec find(
  pos_integer(),
  keyword()
) :: {:ok, pos_integer()} | {:error, refusal()}

The pid of the process inside pasta's namespace, or a refusal.

pasta_pid is what the pidfile contained — the host-side process whose children are searched.

holder(pasta_pid, host_ns, proc \\ "/proc", opts \\ [])

@spec holder(pos_integer(), String.t(), String.t(), keyword()) ::
  {:ok, pos_integer()} | :none

One pass over pasta_pid's children, without waiting.

Public so the identification rule is testable against a synthetic /proc on any host — this cannot run outside Linux, and left private the rule that matters most would be verifiable only where the whole launch works.