Expand description
Host-process resource bounds for every subprocess this runtime spawns.
Sibling of crate::limits, and deliberately not a replacement for it: that module bounds
a WASM guest inside its wasmtime store (linear memory, tables, epoch deadline), this one
bounds the operating-system processes the runtime forks — shell::execute_shell and
runtime::dispatch_native_tool. A capsule that cannot escape its containment can still
wedge the host it runs on: fork bombs, unbounded open files, unbounded writes, unbounded
per-process CPU and address space, and unbounded workdir growth are all denial of service,
not boundary defeats — nothing outside the granted scope is read, written, or reached — but
a wedged host is still a broken host.
Three mechanisms, in descending order of portability:
setrlimit(2)ceilings ([apply_hard_rlimits]), applied inside the forked child’spre_execwindow on every Unix platform. Per-process, POSIX-portable, no configuration required of the host.- a cgroup v2 scope ([
crate::cgroup]) around the whole subprocess tree, Linux only. This is what rlimits structurally cannot do:RLIMIT_NPROCis a per-uid ceiling, checked against every thread the uid owns on the host, so it bounds neither this tree nor anything else in particular.pids.maxon a cgroup counts only the scope’s own tasks, and a spawn that joins a scope gets noRLIMIT_NPROCfrom this module at all ([NprocBound]). - a periodic workdir-size check ([
WorkdirGuard]), on every platform. A plain filesystem walk on a fixed cadence, so a breach is caught within one poll interval — not instantly.
§A silent manifest: no CPU limit, a default for everything else
HostResourceLimits::resolve reads the manifest’s capabilities.resources block, and
treats the whole block’s absence as “every field omitted”. The two CPU fields carry over as
declared: an omitted cpu_seconds sets no RLIMIT_CPU, so a subprocess keeps the limit this
process inherited, and an omitted cgroup_cpu_percent writes no cpu.max, so the scope
keeps the kernel’s max. Both change how fast and how long an agent’s work may run, not
whether the host survives it. Every other field replaces an omission with its DEFAULT_*
constant, as crate::limits::ExecutionLimits::resolve does: a capsule that declares none
of them is bounded identically to one that declares every default by hand.
§Hard limits, not soft ones
setrlimit(2) takes both a soft (rlim_cur) and a hard (rlim_max) value, and an
unprivileged process may raise its own soft limit up to the hard one at any time. Against a
hostile agent a soft-only cap is therefore advisory: one setrlimit call from inside the
capsule undoes it. Every limit this module sets writes rlim_cur == rlim_max, leaving no
ceiling to raise into, and lowering rlim_max is irreversible for an unprivileged process.
A set RLIMIT_CPU is the one exception: its soft slot is cpu_seconds and its hard slot one
second above ([cpu_rlimit_slots]). The kernel sends SIGXCPU, the signal
[limit_from_signal] attributes, only while the soft slot sits below the hard one. The bound
is the hard slot: a process that raises its soft slot to the hard one, or ignores SIGXCPU,
gets SIGKILL at cpu_seconds + 1, so CPU time per process stays within cpu_seconds plus
one second.
Structs§
- Host
Resource Limits - Fully-resolved host-process limits for one session: the manifest’s
capabilities.resourcesblock with every omitted field replaced by itsDEFAULT_*, except the two CPU fields, which stayNonewhen omitted.
Constants§
- DEFAULT_
CGROUP_ IO_ BYTES_ PER_ SEC - Default cgroup
io.maxread+write throughput on the workdir’s backing device — 100 MiB/s. - DEFAULT_
CGROUP_ MEMORY_ BYTES - Default cgroup
memory.max— 4 GiB aggregate across the whole subprocess tree. Above the per-process ceiling on purpose: the tree bound exists to catch what many processes do together, so setting it at or below the single-process bound would make it redundant. - DEFAULT_
CGROUP_ PIDS_ MAX - Default cgroup
pids.max— 256 tasks aggregate across the whole subprocess tree. - DEFAULT_
MAX_ FILE_ SIZE_ BYTES - Default
RLIMIT_FSIZEhard ceiling — 4 GiB, the largest single file a subprocess may write. - DEFAULT_
MAX_ OPEN_ FILES - Default
RLIMIT_NOFILEhard ceiling — open descriptors per spawned subprocess. - DEFAULT_
MAX_ PROCESSES - Default
RLIMIT_NPROCheadroom for a spawn with no cgroup scope — how much past the uid’s count at that spawn a subprocess tree may add, in whichever unit this platform’sRLIMIT_NPROCis checked against. Unused for a spawn that joins a scope. See [NprocBound] for which spawns get it and [uid_task_count] for the per-platform unit. - DEFAULT_
MEMORY_ BYTES - Default
RLIMIT_AS(Linux) /RLIMIT_DATA(macOS) hard ceiling — 2 GiB per subprocess. - DEFAULT_
WORKDIR_ MAX_ BYTES - Default ceiling on total workdir size — 10 GiB, checked periodically.
- V8_
ADDRESS_ SPACE_ FLOOR_ BYTES - The address-space limit below which
W-RUN-010warns for a V8 binary: 32 GiB. V8 reserves far more address space than it uses, andnodefailed with “Cannot allocate Wasm memory” at 16 GiB and ran at 32 GiB underulimit -v. - V8_
BINARY_ NAMES - The
capabilities.shell.allowfile namesW-RUN-010treats as V8.
Functions§
- v8_
address_ space_ warning W-RUN-010as staging andmur doctorprint it, orNonewhen it does not fire.